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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-02-20 16:49:51 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-02-20 16:49:51 +0300
commit71786ddc8e28fbd3cb3fcc4b3ff15e5962a1c82e (patch)
tree6a2d93ef3fb2d353bb7739e4b57e6541f51cdd71 /spec/models
parenta7253423e3403b8c08f8a161e5937e1488f5f407 (diff)
Add latest changes from gitlab-org/gitlab@15-9-stable-eev15.9.0-rc42
Diffstat (limited to 'spec/models')
-rw-r--r--spec/models/abuse_report_spec.rb35
-rw-r--r--spec/models/achievements/achievement_spec.rb2
-rw-r--r--spec/models/achievements/user_achievement_spec.rb2
-rw-r--r--spec/models/airflow/dags_spec.rb17
-rw-r--r--spec/models/analytics/cycle_analytics/aggregation_spec.rb10
-rw-r--r--spec/models/analytics/cycle_analytics/project_stage_spec.rb58
-rw-r--r--spec/models/analytics/cycle_analytics/project_value_stream_spec.rb39
-rw-r--r--spec/models/analytics/cycle_analytics/stage_event_hash_spec.rb8
-rw-r--r--spec/models/analytics/cycle_analytics/stage_spec.rb135
-rw-r--r--spec/models/analytics/cycle_analytics/value_stream_spec.rb75
-rw-r--r--spec/models/appearance_spec.rb54
-rw-r--r--spec/models/approval_spec.rb2
-rw-r--r--spec/models/bulk_imports/entity_spec.rb69
-rw-r--r--spec/models/ci/bridge_spec.rb40
-rw-r--r--spec/models/ci/build_metadata_spec.rb4
-rw-r--r--spec/models/ci/build_pending_state_spec.rb9
-rw-r--r--spec/models/ci/build_spec.rb504
-rw-r--r--spec/models/ci/group_variable_spec.rb7
-rw-r--r--spec/models/ci/job_artifact_spec.rb31
-rw-r--r--spec/models/ci/job_token/allowlist_spec.rb39
-rw-r--r--spec/models/ci/job_token/project_scope_link_spec.rb29
-rw-r--r--spec/models/ci/job_token/scope_spec.rb165
-rw-r--r--spec/models/ci/pipeline_spec.rb322
-rw-r--r--spec/models/ci/processable_spec.rb10
-rw-r--r--spec/models/ci/runner_machine_spec.rb158
-rw-r--r--spec/models/ci/runner_spec.rb67
-rw-r--r--spec/models/ci/runner_version_spec.rb18
-rw-r--r--spec/models/ci/running_build_spec.rb2
-rw-r--r--spec/models/ci/secure_file_spec.rb5
-rw-r--r--spec/models/ci/trigger_spec.rb38
-rw-r--r--spec/models/ci/variable_spec.rb7
-rw-r--r--spec/models/ci_platform_metric_spec.rb2
-rw-r--r--spec/models/clusters/applications/cert_manager_spec.rb157
-rw-r--r--spec/models/clusters/applications/cilium_spec.rb17
-rw-r--r--spec/models/clusters/cluster_spec.rb230
-rw-r--r--spec/models/commit_status_spec.rb98
-rw-r--r--spec/models/concerns/after_commit_queue_spec.rb2
-rw-r--r--spec/models/concerns/bulk_insert_safe_spec.rb2
-rw-r--r--spec/models/concerns/ci/has_status_spec.rb2
-rw-r--r--spec/models/concerns/ci/has_variable_spec.rb34
-rw-r--r--spec/models/concerns/ci/maskable_spec.rb116
-rw-r--r--spec/models/concerns/cross_database_modification_spec.rb8
-rw-r--r--spec/models/concerns/exportable_spec.rb236
-rw-r--r--spec/models/concerns/issuable_link_spec.rb14
-rw-r--r--spec/models/concerns/noteable_spec.rb65
-rw-r--r--spec/models/concerns/pg_full_text_searchable_spec.rb2
-rw-r--r--spec/models/concerns/require_email_verification_spec.rb17
-rw-r--r--spec/models/concerns/sensitive_serializable_hash_spec.rb4
-rw-r--r--spec/models/concerns/spammable_spec.rb16
-rw-r--r--spec/models/concerns/taskable_spec.rb12
-rw-r--r--spec/models/concerns/triggerable_hooks_spec.rb2
-rw-r--r--spec/models/container_registry/event_spec.rb71
-rw-r--r--spec/models/container_repository_spec.rb2
-rw-r--r--spec/models/cycle_analytics/project_level_stage_adapter_spec.rb9
-rw-r--r--spec/models/deploy_key_spec.rb4
-rw-r--r--spec/models/deployment_spec.rb17
-rw-r--r--spec/models/design_management/design_spec.rb2
-rw-r--r--spec/models/discussion_spec.rb65
-rw-r--r--spec/models/environment_spec.rb37
-rw-r--r--spec/models/event_spec.rb2
-rw-r--r--spec/models/factories_spec.rb211
-rw-r--r--spec/models/group_spec.rb121
-rw-r--r--spec/models/hooks/project_hook_spec.rb120
-rw-r--r--spec/models/hooks/service_hook_spec.rb38
-rw-r--r--spec/models/hooks/system_hook_spec.rb12
-rw-r--r--spec/models/hooks/web_hook_log_spec.rb31
-rw-r--r--spec/models/hooks/web_hook_spec.rb402
-rw-r--r--spec/models/incident_management/timeline_event_tag_spec.rb12
-rw-r--r--spec/models/integration_spec.rb8
-rw-r--r--spec/models/integrations/base_chat_notification_spec.rb30
-rw-r--r--spec/models/integrations/issue_tracker_data_spec.rb2
-rw-r--r--spec/models/integrations/jira_tracker_data_spec.rb2
-rw-r--r--spec/models/integrations/microsoft_teams_spec.rb4
-rw-r--r--spec/models/integrations/mock_ci_spec.rb4
-rw-r--r--spec/models/integrations/zentao_tracker_data_spec.rb2
-rw-r--r--spec/models/issue_email_participant_spec.rb6
-rw-r--r--spec/models/issue_spec.rb12
-rw-r--r--spec/models/jira_connect_installation_spec.rb8
-rw-r--r--spec/models/key_spec.rb8
-rw-r--r--spec/models/member_spec.rb16
-rw-r--r--spec/models/members/group_member_spec.rb8
-rw-r--r--spec/models/members/member_role_spec.rb26
-rw-r--r--spec/models/members/project_member_spec.rb22
-rw-r--r--spec/models/merge_request/cleanup_schedule_spec.rb2
-rw-r--r--spec/models/merge_request_diff_commit_spec.rb2
-rw-r--r--spec/models/merge_request_diff_file_spec.rb2
-rw-r--r--spec/models/merge_request_spec.rb118
-rw-r--r--spec/models/ml/candidate_spec.rb69
-rw-r--r--spec/models/ml/experiment_spec.rb17
-rw-r--r--spec/models/namespace/traversal_hierarchy_spec.rb2
-rw-r--r--spec/models/namespace_setting_spec.rb30
-rw-r--r--spec/models/namespace_spec.rb154
-rw-r--r--spec/models/namespaces/randomized_suffix_path_spec.rb37
-rw-r--r--spec/models/note_spec.rb42
-rw-r--r--spec/models/onboarding/learn_gitlab_spec.rb69
-rw-r--r--spec/models/packages/composer/metadatum_spec.rb29
-rw-r--r--spec/models/packages/debian/file_entry_spec.rb7
-rw-r--r--spec/models/packages/package_spec.rb28
-rw-r--r--spec/models/packages/tag_spec.rb12
-rw-r--r--spec/models/personal_access_token_spec.rb52
-rw-r--r--spec/models/preloaders/user_max_access_level_in_groups_preloader_spec.rb37
-rw-r--r--spec/models/preloaders/user_max_access_level_in_projects_preloader_spec.rb14
-rw-r--r--spec/models/project_authorization_spec.rb15
-rw-r--r--spec/models/project_ci_cd_setting_spec.rb18
-rw-r--r--spec/models/project_feature_spec.rb24
-rw-r--r--spec/models/project_import_state_spec.rb55
-rw-r--r--spec/models/project_setting_spec.rb50
-rw-r--r--spec/models/project_spec.rb241
-rw-r--r--spec/models/project_team_spec.rb2
-rw-r--r--spec/models/projects/data_transfer_spec.rb62
-rw-r--r--spec/models/protected_branch_spec.rb150
-rw-r--r--spec/models/protected_tag/create_access_level_spec.rb144
-rw-r--r--spec/models/release_highlight_spec.rb3
-rw-r--r--spec/models/release_spec.rb15
-rw-r--r--spec/models/repository_spec.rb97
-rw-r--r--spec/models/resource_event_spec.rb2
-rw-r--r--spec/models/resource_label_event_spec.rb2
-rw-r--r--spec/models/resource_milestone_event_spec.rb2
-rw-r--r--spec/models/resource_state_event_spec.rb2
-rw-r--r--spec/models/service_desk_setting_spec.rb65
-rw-r--r--spec/models/user_detail_spec.rb31
-rw-r--r--spec/models/user_spec.rb21
-rw-r--r--spec/models/wiki_directory_spec.rb9
-rw-r--r--spec/models/wiki_page_spec.rb2
-rw-r--r--spec/models/work_item_spec.rb112
-rw-r--r--spec/models/work_items/type_spec.rb73
-rw-r--r--spec/models/work_items/widget_definition_spec.rb92
-rw-r--r--spec/models/work_items/widgets/assignees_spec.rb6
-rw-r--r--spec/models/work_items/widgets/hierarchy_spec.rb30
-rw-r--r--spec/models/work_items/widgets/labels_spec.rb6
-rw-r--r--spec/models/work_items/widgets/start_and_due_date_spec.rb6
131 files changed, 4346 insertions, 2025 deletions
diff --git a/spec/models/abuse_report_spec.rb b/spec/models/abuse_report_spec.rb
index b07fafabbb5..7995cc36383 100644
--- a/spec/models/abuse_report_spec.rb
+++ b/spec/models/abuse_report_spec.rb
@@ -43,6 +43,41 @@ RSpec.describe AbuseReport, feature_category: :insider_threat do
it { is_expected.not_to allow_value(javascript).for(:reported_from_url) }
it { is_expected.to allow_value('http://localhost:9000').for(:reported_from_url) }
it { is_expected.to allow_value('https://gitlab.com').for(:reported_from_url) }
+
+ it { is_expected.to allow_value([]).for(:links_to_spam) }
+ it { is_expected.to allow_value(nil).for(:links_to_spam) }
+ it { is_expected.to allow_value('').for(:links_to_spam) }
+
+ it { is_expected.to allow_value(['https://gitlab.com']).for(:links_to_spam) }
+ it { is_expected.to allow_value(['http://localhost:9000']).for(:links_to_spam) }
+
+ it { is_expected.not_to allow_value(['spam']).for(:links_to_spam) }
+ it { is_expected.not_to allow_value(['http://localhost:9000', 'spam']).for(:links_to_spam) }
+
+ it { is_expected.to allow_value(['https://gitlab.com'] * 20).for(:links_to_spam) }
+ it { is_expected.not_to allow_value(['https://gitlab.com'] * 21).for(:links_to_spam) }
+
+ it {
+ is_expected.to allow_value([
+ "https://gitlab.com/#{SecureRandom.alphanumeric(493)}"
+ ]).for(:links_to_spam)
+ }
+
+ it {
+ is_expected.not_to allow_value([
+ "https://gitlab.com/#{SecureRandom.alphanumeric(494)}"
+ ]).for(:links_to_spam)
+ }
+ end
+
+ describe 'before_validation' do
+ context 'when links to spam contains empty strings' do
+ let(:report) { create(:abuse_report, links_to_spam: ['', 'https://gitlab.com']) }
+
+ it 'removes empty strings' do
+ expect(report.links_to_spam).to match_array(['https://gitlab.com'])
+ end
+ end
end
describe '#remove_user' do
diff --git a/spec/models/achievements/achievement_spec.rb b/spec/models/achievements/achievement_spec.rb
index 9a5f4eee229..d3e3e40fc0c 100644
--- a/spec/models/achievements/achievement_spec.rb
+++ b/spec/models/achievements/achievement_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Achievements::Achievement, type: :model, feature_category: :users do
+RSpec.describe Achievements::Achievement, type: :model, feature_category: :user_profile do
describe 'associations' do
it { is_expected.to belong_to(:namespace).required }
diff --git a/spec/models/achievements/user_achievement_spec.rb b/spec/models/achievements/user_achievement_spec.rb
index a91cba2b5e2..9d88bfdd477 100644
--- a/spec/models/achievements/user_achievement_spec.rb
+++ b/spec/models/achievements/user_achievement_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Achievements::UserAchievement, type: :model, feature_category: :users do
+RSpec.describe Achievements::UserAchievement, type: :model, feature_category: :user_profile do
describe 'associations' do
it { is_expected.to belong_to(:achievement).inverse_of(:user_achievements).required }
it { is_expected.to belong_to(:user).inverse_of(:user_achievements).required }
diff --git a/spec/models/airflow/dags_spec.rb b/spec/models/airflow/dags_spec.rb
new file mode 100644
index 00000000000..ff3c4522779
--- /dev/null
+++ b/spec/models/airflow/dags_spec.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Airflow::Dags, feature_category: :dataops do
+ describe 'associations' do
+ it { is_expected.to belong_to(:project) }
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:project) }
+ it { is_expected.to validate_presence_of(:dag_name) }
+ it { is_expected.to validate_length_of(:dag_name).is_at_most(255) }
+ it { is_expected.to validate_length_of(:schedule).is_at_most(255) }
+ it { is_expected.to validate_length_of(:fileloc).is_at_most(255) }
+ end
+end
diff --git a/spec/models/analytics/cycle_analytics/aggregation_spec.rb b/spec/models/analytics/cycle_analytics/aggregation_spec.rb
index a51c21dc87e..e69093f454a 100644
--- a/spec/models/analytics/cycle_analytics/aggregation_spec.rb
+++ b/spec/models/analytics/cycle_analytics/aggregation_spec.rb
@@ -158,6 +158,16 @@ RSpec.describe Analytics::CycleAnalytics::Aggregation, type: :model, feature_cat
end.not_to change { described_class.count }
end
end
+
+ context 'when the aggregation was disabled for some reason' do
+ it 're-enables the aggregation' do
+ create(:cycle_analytics_aggregation, enabled: false, namespace: group)
+
+ aggregation = described_class.safe_create_for_namespace(group)
+
+ expect(aggregation).to be_enabled
+ end
+ end
end
describe '#load_batch' do
diff --git a/spec/models/analytics/cycle_analytics/project_stage_spec.rb b/spec/models/analytics/cycle_analytics/project_stage_spec.rb
deleted file mode 100644
index 3c7fde17355..00000000000
--- a/spec/models/analytics/cycle_analytics/project_stage_spec.rb
+++ /dev/null
@@ -1,58 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Analytics::CycleAnalytics::ProjectStage do
- describe 'associations' do
- it { is_expected.to belong_to(:project).required }
- end
-
- it 'default stages must be valid' do
- project = build(:project)
-
- Gitlab::Analytics::CycleAnalytics::DefaultStages.all.each do |params|
- stage = described_class.new(params.merge(project: project))
- expect(stage).to be_valid
- end
- end
-
- it_behaves_like 'value stream analytics stage' do
- let(:factory) { :cycle_analytics_project_stage }
- let(:parent) { build(:project) }
- let(:parent_name) { :project }
- end
-
- context 'relative positioning' do
- it_behaves_like 'a class that supports relative positioning' do
- let_it_be(:project) { create(:project) }
- let(:factory) { :cycle_analytics_project_stage }
- let(:default_params) { { project: project } }
- end
- end
-
- describe '.distinct_stages_within_hierarchy' do
- let_it_be(:top_level_group) { create(:group) }
- let_it_be(:sub_group_1) { create(:group, parent: top_level_group) }
- let_it_be(:sub_group_2) { create(:group, parent: sub_group_1) }
-
- let_it_be(:project_1) { create(:project, group: sub_group_1) }
- let_it_be(:project_2) { create(:project, group: sub_group_2) }
- let_it_be(:project_3) { create(:project, group: top_level_group) }
-
- let_it_be(:stage1) { create(:cycle_analytics_project_stage, project: project_1, start_event_identifier: :issue_created, end_event_identifier: :issue_deployed_to_production) }
- let_it_be(:stage2) { create(:cycle_analytics_project_stage, project: project_3, start_event_identifier: :issue_created, end_event_identifier: :issue_deployed_to_production) }
-
- let_it_be(:stage3) { create(:cycle_analytics_project_stage, project: project_1, start_event_identifier: :merge_request_created, end_event_identifier: :merge_request_merged) }
- let_it_be(:stage4) { create(:cycle_analytics_project_stage, project: project_3, start_event_identifier: :merge_request_created, end_event_identifier: :merge_request_merged) }
-
- subject(:distinct_start_and_end_event_identifiers) { described_class.distinct_stages_within_hierarchy(top_level_group).to_a.pluck(:start_event_identifier, :end_event_identifier) }
-
- it 'returns distinct stages by start and end events (using stage_event_hash_id)' do
- expect(distinct_start_and_end_event_identifiers).to match_array(
- [
- %w[issue_created issue_deployed_to_production],
- %w[merge_request_created merge_request_merged]
- ])
- end
- end
-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
deleted file mode 100644
index d84ecedc634..00000000000
--- a/spec/models/analytics/cycle_analytics/project_value_stream_spec.rb
+++ /dev/null
@@ -1,39 +0,0 @@
-# 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/analytics/cycle_analytics/stage_event_hash_spec.rb b/spec/models/analytics/cycle_analytics/stage_event_hash_spec.rb
index ffddaf1e1b2..43db610af5c 100644
--- a/spec/models/analytics/cycle_analytics/stage_event_hash_spec.rb
+++ b/spec/models/analytics/cycle_analytics/stage_event_hash_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe Analytics::CycleAnalytics::StageEventHash, type: :model do
let(:hash_sha256) { 'does_not_matter' }
describe 'associations' do
- it { is_expected.to have_many(:cycle_analytics_project_stages) }
+ it { is_expected.to have_many(:cycle_analytics_stages) }
end
describe 'validations' do
@@ -30,14 +30,14 @@ RSpec.describe Analytics::CycleAnalytics::StageEventHash, type: :model do
end
describe '.cleanup_if_unused' do
- it 'removes the record' do
+ it 'removes the record if there is no stages with given stage events hash' do
described_class.cleanup_if_unused(stage_event_hash.id)
expect(described_class.find_by_id(stage_event_hash.id)).to be_nil
end
- it 'does not remove the record' do
- id = create(:cycle_analytics_project_stage).stage_event_hash_id
+ it 'does not remove the record if at least 1 group stage for the given stage events hash exists' do
+ id = create(:cycle_analytics_stage).stage_event_hash_id
described_class.cleanup_if_unused(id)
diff --git a/spec/models/analytics/cycle_analytics/stage_spec.rb b/spec/models/analytics/cycle_analytics/stage_spec.rb
new file mode 100644
index 00000000000..57748f8942e
--- /dev/null
+++ b/spec/models/analytics/cycle_analytics/stage_spec.rb
@@ -0,0 +1,135 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Analytics::CycleAnalytics::Stage, feature_category: :value_stream_management do
+ describe 'uniqueness validation on name' do
+ subject { build(:cycle_analytics_stage) }
+
+ it { is_expected.to validate_uniqueness_of(:name).scoped_to([:group_id, :group_value_stream_id]) }
+ end
+
+ describe 'associations' do
+ it { is_expected.to belong_to(:namespace).required }
+ it { is_expected.to belong_to(:value_stream) }
+ end
+
+ it_behaves_like 'value stream analytics namespace models' do
+ let(:factory_name) { :cycle_analytics_stage }
+ end
+
+ it_behaves_like 'value stream analytics stage' do
+ let(:factory) { :cycle_analytics_stage }
+ let(:parent) { create(:group) }
+ let(:parent_name) { :namespace }
+ end
+
+ describe '.distinct_stages_within_hierarchy' do
+ let_it_be(:group) { create(:group) }
+ let_it_be(:sub_group) { create(:group, parent: group) }
+ let_it_be(:project) { create(:project, group: sub_group).reload }
+
+ before do
+ # event identifiers are the same
+ create(:cycle_analytics_stage, name: 'Stage A1', namespace: group,
+ start_event_identifier: :merge_request_created, end_event_identifier: :merge_request_merged)
+ create(:cycle_analytics_stage, name: 'Stage A2', namespace: sub_group,
+ start_event_identifier: :merge_request_created, end_event_identifier: :merge_request_merged)
+ create(:cycle_analytics_stage, name: 'Stage A3', namespace: sub_group,
+ start_event_identifier: :merge_request_created, end_event_identifier: :merge_request_merged)
+ create(:cycle_analytics_stage, name: 'Stage A4', project: project,
+ start_event_identifier: :merge_request_created, end_event_identifier: :merge_request_merged)
+
+ create(:cycle_analytics_stage,
+ name: 'Stage B1',
+ namespace: group,
+ start_event_identifier: :merge_request_last_build_started,
+ end_event_identifier: :merge_request_last_build_finished)
+
+ create(:cycle_analytics_stage, name: 'Stage C1', project: project,
+ start_event_identifier: :issue_created, end_event_identifier: :issue_deployed_to_production)
+ create(:cycle_analytics_stage, name: 'Stage C2', project: project,
+ start_event_identifier: :issue_created, end_event_identifier: :issue_deployed_to_production)
+ end
+
+ it 'returns distinct stages by the event identifiers' do
+ stages = described_class.distinct_stages_within_hierarchy(group).to_a
+
+ expected_event_pairs = [
+ %w[merge_request_created merge_request_merged],
+ %w[merge_request_last_build_started merge_request_last_build_finished],
+ %w[issue_created issue_deployed_to_production]
+ ].sort
+
+ current_event_pairs = stages.map do |stage|
+ [stage.start_event_identifier, stage.end_event_identifier]
+ end.sort
+
+ expect(current_event_pairs).to eq(expected_event_pairs)
+ end
+ end
+
+ describe 'events tracking' do
+ let(:category) { described_class.to_s }
+ let(:label) { described_class.table_name }
+ let(:namespace) { create(:group) }
+ let(:action) { "database_event_#{property}" }
+ let(:value_stream) { create(:cycle_analytics_value_stream) }
+ let(:feature_flag_name) { :product_intelligence_database_event_tracking }
+ let(:stage) { described_class.create!(stage_params) }
+ let(:stage_params) do
+ {
+ namespace: namespace,
+ name: 'st1',
+ start_event_identifier: :merge_request_created,
+ end_event_identifier: :merge_request_merged,
+ group_value_stream_id: value_stream.id
+ }
+ end
+
+ let(:record_tracked_attributes) do
+ {
+ "id" => stage.id,
+ "created_at" => stage.created_at,
+ "updated_at" => stage.updated_at,
+ "relative_position" => stage.relative_position,
+ "start_event_identifier" => stage.start_event_identifier,
+ "end_event_identifier" => stage.end_event_identifier,
+ "group_id" => stage.group_id,
+ "start_event_label_id" => stage.start_event_label_id,
+ "end_event_label_id" => stage.end_event_label_id,
+ "hidden" => stage.hidden,
+ "custom" => stage.custom,
+ "name" => stage.name,
+ "group_value_stream_id" => stage.group_value_stream_id
+ }
+ end
+
+ describe '#create' do
+ it_behaves_like 'Snowplow event tracking' do
+ let(:property) { 'create' }
+ let(:extra) { record_tracked_attributes }
+
+ subject(:new_group_stage) { stage }
+ end
+ end
+
+ describe '#update', :freeze_time do
+ it_behaves_like 'Snowplow event tracking' do
+ subject(:create_group_stage) { stage.update!(name: 'st 2') }
+
+ let(:extra) { record_tracked_attributes.merge('name' => 'st 2') }
+ let(:property) { 'update' }
+ end
+ end
+
+ describe '#destroy' do
+ it_behaves_like 'Snowplow event tracking' do
+ subject(:delete_stage_group) { stage.destroy! }
+
+ let(:extra) { record_tracked_attributes }
+ let(:property) { 'destroy' }
+ end
+ end
+ end
+end
diff --git a/spec/models/analytics/cycle_analytics/value_stream_spec.rb b/spec/models/analytics/cycle_analytics/value_stream_spec.rb
new file mode 100644
index 00000000000..e32fbef30ae
--- /dev/null
+++ b/spec/models/analytics/cycle_analytics/value_stream_spec.rb
@@ -0,0 +1,75 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Analytics::CycleAnalytics::ValueStream, type: :model, feature_category: :value_stream_management do
+ describe 'associations' do
+ it { is_expected.to belong_to(:namespace).required }
+ it { is_expected.to have_many(:stages) }
+ end
+
+ describe 'validations' do
+ 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
+ group = create(:group)
+ create(:cycle_analytics_value_stream, name: 'test', namespace: group)
+
+ value_stream = build(:cycle_analytics_value_stream, name: 'test', namespace: group)
+
+ expect(value_stream).to be_invalid
+ expect(value_stream.errors.messages).to eq(name: [I18n.t('errors.messages.taken')])
+ end
+
+ it_behaves_like 'value stream analytics namespace models' do
+ let(:factory_name) { :cycle_analytics_value_stream }
+ end
+ end
+
+ describe 'ordering of stages' do
+ let(:group) { create(:group) }
+ let(:value_stream) do
+ create(:cycle_analytics_value_stream, namespace: group, stages: [
+ create(:cycle_analytics_stage, namespace: group, name: "stage 1", relative_position: 5),
+ create(:cycle_analytics_stage, namespace: group, name: "stage 2", relative_position: nil),
+ create(:cycle_analytics_stage, namespace: group, name: "stage 3", relative_position: 1)
+ ])
+ end
+
+ before do
+ value_stream.reload
+ end
+
+ describe 'stages attribute' do
+ it 'sorts stages by relative position' do
+ names = value_stream.stages.map(&:name)
+ expect(names).to eq(['stage 3', 'stage 1', 'stage 2'])
+ end
+ end
+ end
+
+ describe '#custom?' do
+ context 'when value stream is not persisted' do
+ subject(:value_stream) { build(:cycle_analytics_value_stream, name: value_stream_name) }
+
+ context 'when the name of the value stream is default' do
+ let(:value_stream_name) { Analytics::CycleAnalytics::Stages::BaseService::DEFAULT_VALUE_STREAM_NAME }
+
+ it { is_expected.not_to be_custom }
+ end
+
+ context 'when the name of the value stream is not default' do
+ let(:value_stream_name) { 'value_stream_1' }
+
+ it { is_expected.to be_custom }
+ end
+ end
+
+ context 'when value stream is persisted' do
+ subject(:value_stream) { create(:cycle_analytics_value_stream, name: 'value_stream_1') }
+
+ it { is_expected.to be_custom }
+ end
+ end
+end
diff --git a/spec/models/appearance_spec.rb b/spec/models/appearance_spec.rb
index 54dc280d7ac..b5f47c950b9 100644
--- a/spec/models/appearance_spec.rb
+++ b/spec/models/appearance_spec.rb
@@ -3,6 +3,7 @@
require 'spec_helper'
RSpec.describe Appearance do
+ using RSpec::Parameterized::TableSyntax
subject { build(:appearance) }
it { include(CacheableAttributes) }
@@ -14,8 +15,10 @@ RSpec.describe Appearance do
subject(:appearance) { described_class.new }
it { expect(appearance.title).to eq('') }
- it { expect(appearance.pwa_short_name).to eq('') }
it { expect(appearance.description).to eq('') }
+ it { expect(appearance.pwa_name).to eq('') }
+ it { expect(appearance.pwa_short_name).to eq('') }
+ it { expect(appearance.pwa_description).to eq('') }
it { expect(appearance.new_project_guidelines).to eq('') }
it { expect(appearance.profile_image_guidelines).to eq('') }
it { expect(appearance.header_message).to eq('') }
@@ -23,6 +26,7 @@ RSpec.describe Appearance do
it { expect(appearance.message_background_color).to eq('#E75E40') }
it { expect(appearance.message_font_color).to eq('#FFFFFF') }
it { expect(appearance.email_header_and_footer_enabled).to eq(false) }
+ it { expect(Appearance::ALLOWED_PWA_ICON_SCALER_WIDTHS).to match_array([192, 512]) }
end
describe '#single_appearance_row' do
@@ -81,6 +85,19 @@ RSpec.describe Appearance do
it_behaves_like 'logo paths', logo_type
end
+ shared_examples 'icon paths sized' do |width|
+ let_it_be(:appearance) { create(:appearance, :with_pwa_icon) }
+ let_it_be(:filename) { 'dk.png' }
+ let_it_be(:expected_path) { "/uploads/-/system/appearance/pwa_icon/#{appearance.id}/#{filename}?width=#{width}" }
+
+ it 'returns icon path with size parameter' do
+ expect(appearance.pwa_icon_path_scaled(width)).to eq(expected_path)
+ end
+ end
+
+ it_behaves_like 'icon paths sized', 192
+ it_behaves_like 'icon paths sized', 512
+
describe 'validations' do
let(:triplet) { '#000' }
let(:hex) { '#AABBCC' }
@@ -96,6 +113,41 @@ RSpec.describe Appearance do
it { is_expected.not_to allow_value('000').for(:message_font_color) }
end
+ shared_examples 'validation allows' do
+ it { is_expected.to allow_value(value).for(attribute) }
+ end
+
+ shared_examples 'validation permits with message' do
+ it { is_expected.not_to allow_value(value).for(attribute).with_message(message) }
+ end
+
+ context 'valid pwa attributes' do
+ where(:attribute, :value) do
+ :pwa_name | nil
+ :pwa_name | "G" * 255
+ :pwa_short_name | nil
+ :pwa_short_name | "S" * 255
+ :pwa_description | nil
+ :pwa_description | "T" * 2048
+ end
+
+ with_them do
+ it_behaves_like 'validation allows'
+ end
+ end
+
+ context 'invalid pwa attributes' do
+ where(:attribute, :value, :message) do
+ :pwa_name | "G" * 256 | 'is too long (maximum is 255 characters)'
+ :pwa_short_name | "S" * 256 | 'is too long (maximum is 255 characters)'
+ :pwa_description | "T" * 2049 | 'is too long (maximum is 2048 characters)'
+ end
+
+ with_them do
+ it_behaves_like 'validation permits with message'
+ end
+ end
+
describe 'email_header_and_footer_enabled' do
context 'default email_header_and_footer_enabled flag value' do
it 'returns email_header_and_footer_enabled as true' do
diff --git a/spec/models/approval_spec.rb b/spec/models/approval_spec.rb
index e2c0d5faa07..3d382c1712a 100644
--- a/spec/models/approval_spec.rb
+++ b/spec/models/approval_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Approval do
+RSpec.describe Approval, feature_category: :code_review_workflow do
context 'presence validation' do
it { is_expected.to validate_presence_of(:merge_request_id) }
it { is_expected.to validate_presence_of(:user_id) }
diff --git a/spec/models/bulk_imports/entity_spec.rb b/spec/models/bulk_imports/entity_spec.rb
index b1c65c6b9ee..56796aa1fe4 100644
--- a/spec/models/bulk_imports/entity_spec.rb
+++ b/spec/models/bulk_imports/entity_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe BulkImports::Entity, type: :model do
+RSpec.describe BulkImports::Entity, type: :model, feature_category: :importers do
describe 'associations' do
it { is_expected.to belong_to(:bulk_import).required }
it { is_expected.to belong_to(:parent) }
@@ -17,6 +17,38 @@ RSpec.describe BulkImports::Entity, type: :model do
it { is_expected.to define_enum_for(:source_type).with_values(%i[group_entity project_entity]) }
+ context 'when formatting with regexes' do
+ subject { described_class.new(group: Group.new) }
+
+ it { is_expected.to allow_values('namespace', 'parent/namespace', 'parent/group/subgroup', '').for(:destination_namespace) }
+ it { is_expected.not_to allow_values('parent/namespace/', '/namespace', 'parent group/subgroup', '@namespace').for(:destination_namespace) }
+
+ it { is_expected.to allow_values('source', 'source/path', 'source/full/path').for(:source_full_path) }
+ it { is_expected.not_to allow_values('/source', 'http://source/path', 'sou rce/full/path', '').for(:source_full_path) }
+
+ it { is_expected.to allow_values('destination', 'destination-slug', 'new-destination-slug').for(:destination_slug) }
+
+ # it { is_expected.not_to allow_values('destination/slug', '/destination-slug', 'destination slug').for(:destination_slug) } <-- this test should
+ # succeed but it's failing possibly due to rspec caching. To ensure this case is covered see the more cumbersome test below:
+ context 'when destination_slug is invalid' do
+ let(:invalid_slugs) { ['destination/slug', '/destination-slug', 'destination slug'] }
+ let(:error_message) do
+ 'cannot start with a non-alphanumeric character except for periods or underscores, ' \
+ 'can contain only alphanumeric characters, periods, and underscores, ' \
+ 'cannot end with a period or forward slash, and has no ' \
+ 'leading or trailing forward slashes'
+ end
+
+ it 'raises an error' do
+ invalid_slugs.each do |slug|
+ entity = build(:bulk_import_entity, :group_entity, group: build(:group), project: nil, destination_slug: slug)
+ expect(entity).not_to be_valid
+ expect(entity.errors.errors[0].message).to include(error_message)
+ end
+ end
+ end
+ end
+
context 'when associated with a group and project' do
it 'is invalid' do
entity = build(:bulk_import_entity, group: build(:group), project: build(:project))
@@ -45,6 +77,21 @@ RSpec.describe BulkImports::Entity, type: :model do
expect(entity).to be_valid
end
+ it 'is invalid when destination_namespace is nil' do
+ entity = build(:bulk_import_entity, :group_entity, group: build(:group), project: nil, destination_namespace: nil)
+ expect(entity).not_to be_valid
+ end
+
+ it 'is invalid when destination_slug is empty' do
+ entity = build(:bulk_import_entity, :group_entity, group: build(:group), project: nil, destination_slug: '')
+ expect(entity).not_to be_valid
+ end
+
+ it 'is invalid when destination_slug is nil' do
+ entity = build(:bulk_import_entity, :group_entity, group: build(:group), project: nil, destination_slug: nil)
+ expect(entity).not_to be_valid
+ end
+
it 'is invalid as a project_entity' do
stub_feature_flags(bulk_import_projects: true)
@@ -345,4 +392,24 @@ RSpec.describe BulkImports::Entity, type: :model do
expect(entity.full_path).to eq(nil)
end
end
+
+ describe '#default_visibility_level' do
+ context 'when entity is a group' do
+ it 'returns default group visibility' do
+ stub_application_setting(default_group_visibility: Gitlab::VisibilityLevel::PUBLIC)
+ entity = build(:bulk_import_entity, :group_entity, group: build(:group))
+
+ expect(entity.default_visibility_level).to eq(Gitlab::VisibilityLevel::PUBLIC)
+ end
+ end
+
+ context 'when entity is a project' do
+ it 'returns default project visibility' do
+ stub_application_setting(default_project_visibility: Gitlab::VisibilityLevel::INTERNAL)
+ entity = build(:bulk_import_entity, :project_entity, group: build(:group))
+
+ expect(entity.default_visibility_level).to eq(Gitlab::VisibilityLevel::INTERNAL)
+ end
+ end
+ end
end
diff --git a/spec/models/ci/bridge_spec.rb b/spec/models/ci/bridge_spec.rb
index 70e977e37ba..7b307de87c7 100644
--- a/spec/models/ci/bridge_spec.rb
+++ b/spec/models/ci/bridge_spec.rb
@@ -37,8 +37,18 @@ RSpec.describe Ci::Bridge, feature_category: :continuous_integration do
describe '#retryable?' do
let(:bridge) { create(:ci_bridge, :success) }
- it 'returns false' do
- expect(bridge.retryable?).to eq(false)
+ it 'returns true' do
+ expect(bridge.retryable?).to eq(true)
+ end
+
+ context 'without ci_recreate_downstream_pipeline ff' do
+ before do
+ stub_feature_flags(ci_recreate_downstream_pipeline: false)
+ end
+
+ it 'returns false' do
+ expect(bridge.retryable?).to eq(false)
+ end
end
end
@@ -570,4 +580,30 @@ RSpec.describe Ci::Bridge, feature_category: :continuous_integration do
end
end
end
+
+ describe 'metadata partitioning', :ci_partitioning do
+ let(:pipeline) { create(:ci_pipeline, project: project, partition_id: ci_testing_partition_id) }
+
+ let(:bridge) do
+ build(:ci_bridge, pipeline: pipeline)
+ end
+
+ it 'creates the metadata record and assigns its partition' do
+ # the factory doesn't use any metadatable setters by default
+ # so the record will be initialized by the before_validation callback
+ expect(bridge.metadata).to be_nil
+
+ expect(bridge.save!).to be_truthy
+
+ expect(bridge.metadata).to be_present
+ expect(bridge.metadata).to be_valid
+ expect(bridge.metadata.partition_id).to eq(ci_testing_partition_id)
+ end
+ end
+
+ describe '#deployment_job?' do
+ subject { bridge.deployment_job? }
+
+ it { is_expected.to eq(false) }
+ end
end
diff --git a/spec/models/ci/build_metadata_spec.rb b/spec/models/ci/build_metadata_spec.rb
index 8bf3af44be6..fb50ba89cd3 100644
--- a/spec/models/ci/build_metadata_spec.rb
+++ b/spec/models/ci/build_metadata_spec.rb
@@ -20,6 +20,10 @@ RSpec.describe Ci::BuildMetadata do
it_behaves_like 'having unique enum values'
+ it { is_expected.to belong_to(:build) }
+ it { is_expected.to belong_to(:project) }
+ it { is_expected.to belong_to(:runner_machine) }
+
describe '#update_timeout_state' do
subject { metadata }
diff --git a/spec/models/ci/build_pending_state_spec.rb b/spec/models/ci/build_pending_state_spec.rb
index 756180621ec..bff0b35f878 100644
--- a/spec/models/ci/build_pending_state_spec.rb
+++ b/spec/models/ci/build_pending_state_spec.rb
@@ -2,7 +2,14 @@
require 'spec_helper'
-RSpec.describe Ci::BuildPendingState do
+RSpec.describe Ci::BuildPendingState, feature_category: :continuous_integration do
+ describe 'validations' do
+ subject(:pending_state) { build(:ci_build_pending_state) }
+
+ it { is_expected.to belong_to(:build) }
+ it { is_expected.to validate_presence_of(:build) }
+ end
+
describe '#crc32' do
context 'when checksum does not exist' do
let(:pending_state) do
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index dd1fbd7d0d5..2b3dc97e06d 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -2,16 +2,16 @@
require 'spec_helper'
-RSpec.describe Ci::Build, feature_category: :continuous_integration do
+RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_default: :keep do
include Ci::TemplateHelpers
include AfterNextHelpers
let_it_be(:user) { create(:user) }
- let_it_be(:group, reload: true) { create(:group) }
- let_it_be(:project, reload: true) { create(:project, :repository, group: group) }
+ let_it_be(:group, reload: true) { create_default(:group) }
+ let_it_be(:project, reload: true) { create_default(:project, :repository, group: group) }
let_it_be(:pipeline, reload: true) do
- create(:ci_pipeline, project: project,
+ create_default(:ci_pipeline, project: project,
sha: project.commit.id,
ref: project.default_branch,
status: 'success')
@@ -23,17 +23,20 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
it { is_expected.to belong_to(:trigger_request) }
it { is_expected.to belong_to(:erased_by) }
- it { is_expected.to have_many(:needs) }
- it { is_expected.to have_many(:sourced_pipelines) }
- it { is_expected.to have_one(:sourced_pipeline) }
- it { is_expected.to have_many(:job_variables) }
- it { is_expected.to have_many(:report_results) }
- it { is_expected.to have_many(:pages_deployments) }
+ it { is_expected.to have_many(:needs).with_foreign_key(:build_id) }
+ it { is_expected.to have_many(:sourced_pipelines).with_foreign_key(:source_job_id) }
+ it { is_expected.to have_one(:sourced_pipeline).with_foreign_key(:source_job_id) }
+ it { is_expected.to have_many(:job_variables).with_foreign_key(:job_id) }
+ it { is_expected.to have_many(:report_results).with_foreign_key(:build_id) }
+ it { is_expected.to have_many(:pages_deployments).with_foreign_key(:ci_build_id) }
it { is_expected.to have_one(:deployment) }
- it { is_expected.to have_one(:runner_session) }
- it { is_expected.to have_one(:trace_metadata) }
- it { is_expected.to have_many(:terraform_state_versions).inverse_of(:build) }
+ it { is_expected.to have_one(:runner_machine).through(:metadata) }
+ it { is_expected.to have_one(:runner_session).with_foreign_key(:build_id) }
+ it { is_expected.to have_one(:trace_metadata).with_foreign_key(:build_id) }
+ it { is_expected.to have_one(:runtime_metadata).with_foreign_key(:build_id) }
+ it { is_expected.to have_one(:pending_state).with_foreign_key(:build_id) }
+ it { is_expected.to have_many(:terraform_state_versions).inverse_of(:build).with_foreign_key(:ci_build_id) }
it { is_expected.to validate_presence_of(:ref) }
@@ -66,7 +69,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
it 'executes hooks' do
expect_next(described_class).to receive(:execute_hooks)
- create(:ci_build)
+ create(:ci_build, pipeline: pipeline)
end
end
end
@@ -105,19 +108,19 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
subject { described_class.ref_protected }
context 'when protected is true' do
- let!(:job) { create(:ci_build, :protected) }
+ let!(:job) { create(:ci_build, :protected, pipeline: pipeline) }
it { is_expected.to include(job) }
end
context 'when protected is false' do
- let!(:job) { create(:ci_build) }
+ let!(:job) { create(:ci_build, pipeline: pipeline) }
it { is_expected.not_to include(job) }
end
context 'when protected is nil' do
- let!(:job) { create(:ci_build) }
+ let!(:job) { create(:ci_build, pipeline: pipeline) }
before do
job.update_attribute(:protected, nil)
@@ -131,7 +134,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
subject { described_class.with_downloadable_artifacts }
context 'when job does not have a downloadable artifact' do
- let!(:job) { create(:ci_build) }
+ let!(:job) { create(:ci_build, pipeline: pipeline) }
it 'does not return the job' do
is_expected.not_to include(job)
@@ -141,7 +144,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
::Ci::JobArtifact::DOWNLOADABLE_TYPES.each do |type|
context "when job has a #{type} artifact" do
it 'returns the job' do
- job = create(:ci_build)
+ job = create(:ci_build, pipeline: pipeline)
create(
:ci_job_artifact,
file_format: ::Ci::JobArtifact::TYPE_AND_FORMAT_PAIRS[type.to_sym],
@@ -155,7 +158,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
context 'when job has a non-downloadable artifact' do
- let!(:job) { create(:ci_build, :trace_artifact) }
+ let!(:job) { create(:ci_build, :trace_artifact, pipeline: pipeline) }
it 'does not return the job' do
is_expected.not_to include(job)
@@ -167,7 +170,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
subject { described_class.with_erasable_artifacts }
context 'when job does not have any artifacts' do
- let!(:job) { create(:ci_build) }
+ let!(:job) { create(:ci_build, pipeline: pipeline) }
it 'does not return the job' do
is_expected.not_to include(job)
@@ -177,7 +180,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
::Ci::JobArtifact.erasable_file_types.each do |type|
context "when job has a #{type} artifact" do
it 'returns the job' do
- job = create(:ci_build)
+ job = create(:ci_build, pipeline: pipeline)
create(
:ci_job_artifact,
file_format: ::Ci::JobArtifact::TYPE_AND_FORMAT_PAIRS[type.to_sym],
@@ -191,7 +194,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
context 'when job has a non-erasable artifact' do
- let!(:job) { create(:ci_build, :trace_artifact) }
+ let!(:job) { create(:ci_build, :trace_artifact, pipeline: pipeline) }
it 'does not return the job' do
is_expected.not_to include(job)
@@ -199,11 +202,39 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
end
+ describe '.with_any_artifacts' do
+ subject { described_class.with_any_artifacts }
+
+ context 'when job does not have any artifacts' do
+ it 'does not return the job' do
+ job = create(:ci_build, project: project)
+
+ is_expected.not_to include(job)
+ end
+ end
+
+ ::Ci::JobArtifact.file_types.each_key do |type|
+ context "when job has a #{type} artifact" do
+ it 'returns the job' do
+ job = create(:ci_build, project: project)
+ create(
+ :ci_job_artifact,
+ file_format: ::Ci::JobArtifact::TYPE_AND_FORMAT_PAIRS[type.to_sym],
+ file_type: type,
+ job: job
+ )
+
+ is_expected.to include(job)
+ end
+ end
+ end
+ end
+
describe '.with_live_trace' do
subject { described_class.with_live_trace }
context 'when build has live trace' do
- let!(:build) { create(:ci_build, :success, :trace_live) }
+ let!(:build) { create(:ci_build, :success, :trace_live, pipeline: pipeline) }
it 'selects the build' do
is_expected.to eq([build])
@@ -211,7 +242,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
context 'when build does not have live trace' do
- let!(:build) { create(:ci_build, :success, :trace_artifact) }
+ let!(:build) { create(:ci_build, :success, :trace_artifact, pipeline: pipeline) }
it 'does not select the build' do
is_expected.to be_empty
@@ -223,7 +254,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
subject { described_class.with_stale_live_trace }
context 'when build has a stale live trace' do
- let!(:build) { create(:ci_build, :success, :trace_live, finished_at: 1.day.ago) }
+ let!(:build) { create(:ci_build, :success, :trace_live, finished_at: 1.day.ago, pipeline: pipeline) }
it 'selects the build' do
is_expected.to eq([build])
@@ -231,7 +262,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
context 'when build does not have a stale live trace' do
- let!(:build) { create(:ci_build, :success, :trace_live, finished_at: 1.hour.ago) }
+ let!(:build) { create(:ci_build, :success, :trace_live, finished_at: 1.hour.ago, pipeline: pipeline) }
it 'does not select the build' do
is_expected.to be_empty
@@ -242,9 +273,9 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
describe '.license_management_jobs' do
subject { described_class.license_management_jobs }
- let!(:management_build) { create(:ci_build, :success, name: :license_management) }
- let!(:scanning_build) { create(:ci_build, :success, name: :license_scanning) }
- let!(:another_build) { create(:ci_build, :success, name: :another_type) }
+ let!(:management_build) { create(:ci_build, :success, name: :license_management, pipeline: pipeline) }
+ let!(:scanning_build) { create(:ci_build, :success, name: :license_scanning, pipeline: pipeline) }
+ let!(:another_build) { create(:ci_build, :success, name: :another_type, pipeline: pipeline) }
it 'returns license_scanning jobs' do
is_expected.to include(scanning_build)
@@ -265,7 +296,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
let(:date) { 1.hour.ago }
context 'when build has finished one day ago' do
- let!(:build) { create(:ci_build, :success, finished_at: 1.day.ago) }
+ let!(:build) { create(:ci_build, :success, finished_at: 1.day.ago, pipeline: pipeline) }
it 'selects the build' do
is_expected.to eq([build])
@@ -273,7 +304,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
context 'when build has finished 30 minutes ago' do
- let!(:build) { create(:ci_build, :success, finished_at: 30.minutes.ago) }
+ let!(:build) { create(:ci_build, :success, finished_at: 30.minutes.ago, pipeline: pipeline) }
it 'returns an empty array' do
is_expected.to be_empty
@@ -281,7 +312,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
context 'when build is still running' do
- let!(:build) { create(:ci_build, :running) }
+ let!(:build) { create(:ci_build, :running, pipeline: pipeline) }
it 'returns an empty array' do
is_expected.to be_empty
@@ -292,9 +323,9 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
describe '.with_exposed_artifacts' do
subject { described_class.with_exposed_artifacts }
- let!(:job1) { create(:ci_build) }
- let!(:job2) { create(:ci_build, options: options) }
- let!(:job3) { create(:ci_build) }
+ let!(:job1) { create(:ci_build, pipeline: pipeline) }
+ let!(:job2) { create(:ci_build, options: options, pipeline: pipeline) }
+ let!(:job3) { create(:ci_build, pipeline: pipeline) }
context 'when some jobs have exposed artifacs and some not' do
let(:options) { { artifacts: { expose_as: 'test', paths: ['test'] } } }
@@ -334,7 +365,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
context 'when there are multiple builds containing artifacts' do
before do
- create_list(:ci_build, 5, :success, :test_reports)
+ create_list(:ci_build, 5, :success, :test_reports, pipeline: pipeline)
end
it 'does not execute a query for selecting job artifact one by one' do
@@ -350,8 +381,8 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
describe '.with_needs' do
- let!(:build) { create(:ci_build) }
- let!(:build_b) { create(:ci_build) }
+ let!(:build) { create(:ci_build, pipeline: pipeline) }
+ let!(:build_b) { create(:ci_build, pipeline: pipeline) }
let!(:build_need_a) { create(:ci_build_need, build: build) }
let!(:build_need_b) { create(:ci_build_need, build: build_b) }
@@ -390,7 +421,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
describe '#stick_build_if_status_changed' do
it 'sticks the build if the status changed' do
- job = create(:ci_build, :pending)
+ job = create(:ci_build, :pending, pipeline: pipeline)
expect(described_class.sticking).to receive(:stick)
.with(:build, job.id)
@@ -400,7 +431,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
describe '#enqueue' do
- let(:build) { create(:ci_build, :created) }
+ let(:build) { create(:ci_build, :created, pipeline: pipeline) }
before do
allow(build).to receive(:any_unmet_prerequisites?).and_return(has_prerequisites)
@@ -477,7 +508,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
describe '#enqueue_preparing' do
- let(:build) { create(:ci_build, :preparing) }
+ let(:build) { create(:ci_build, :preparing, pipeline: pipeline) }
before do
allow(build).to receive(:any_unmet_prerequisites?).and_return(has_unmet_prerequisites)
@@ -532,7 +563,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
describe '#run' do
context 'when build has been just created' do
- let(:build) { create(:ci_build, :created) }
+ let(:build) { create(:ci_build, :created, pipeline: pipeline) }
it 'creates queuing entry and then removes it' do
build.enqueue!
@@ -544,7 +575,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
context 'when build status transition fails' do
- let(:build) { create(:ci_build, :pending) }
+ let(:build) { create(:ci_build, :pending, pipeline: pipeline) }
before do
create(:ci_pending_build, build: build, project: build.project)
@@ -560,7 +591,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
context 'when build has been picked by a shared runner' do
- let(:build) { create(:ci_build, :pending) }
+ let(:build) { create(:ci_build, :pending, pipeline: pipeline) }
it 'creates runtime metadata entry' do
build.runner = create(:ci_runner, :instance_type)
@@ -574,7 +605,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
describe '#drop' do
context 'when has a runtime tracking entry' do
- let(:build) { create(:ci_build, :pending) }
+ let(:build) { create(:ci_build, :pending, pipeline: pipeline) }
it 'removes runtime tracking entry' do
build.runner = create(:ci_runner, :instance_type)
@@ -611,10 +642,10 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
describe '#outdated_deployment?' do
subject { build.outdated_deployment? }
- let(:build) { create(:ci_build, :created, :with_deployment, project: project, environment: 'production') }
+ let(:build) { create(:ci_build, :created, :with_deployment, pipeline: pipeline, environment: 'production') }
context 'when build has no environment' do
- let(:build) { create(:ci_build, :created, project: project, environment: nil) }
+ let(:build) { create(:ci_build, :created, pipeline: pipeline, environment: nil) }
it { expect(subject).to be_falsey }
end
@@ -644,7 +675,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
context 'when build is older than the latest deployment but succeeded once' do
- let(:build) { create(:ci_build, :success, :with_deployment, project: project, environment: 'production') }
+ let(:build) { create(:ci_build, :success, :with_deployment, pipeline: pipeline, environment: 'production') }
before do
allow(build.deployment).to receive(:older_than_last_successful_deployment?).and_return(true)
@@ -660,13 +691,13 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
subject { build.schedulable? }
context 'when build is schedulable' do
- let(:build) { create(:ci_build, :created, :schedulable, project: project) }
+ let(:build) { create(:ci_build, :created, :schedulable, pipeline: pipeline) }
it { expect(subject).to be_truthy }
end
context 'when build is not schedulable' do
- let(:build) { create(:ci_build, :created, project: project) }
+ let(:build) { create(:ci_build, :created, pipeline: pipeline) }
it { expect(subject).to be_falsy }
end
@@ -679,7 +710,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
project.add_developer(user)
end
- let(:build) { create(:ci_build, :created, :schedulable, user: user, project: project) }
+ let(:build) { create(:ci_build, :created, :schedulable, user: user, pipeline: pipeline) }
it 'transits to scheduled' do
allow(Ci::BuildScheduleWorker).to receive(:perform_at)
@@ -740,7 +771,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
describe '#options_scheduled_at' do
subject { build.options_scheduled_at }
- let(:build) { build_stubbed(:ci_build, options: option) }
+ let(:build) { build_stubbed(:ci_build, options: option, pipeline: pipeline) }
context 'when start_in is 1 day' do
let(:option) { { start_in: '1 day' } }
@@ -878,18 +909,18 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
context 'when new artifacts are used' do
context 'artifacts archive does not exist' do
- let(:build) { create(:ci_build) }
+ let(:build) { create(:ci_build, pipeline: pipeline) }
it { is_expected.to be_falsy }
end
context 'artifacts archive exists' do
- let(:build) { create(:ci_build, :artifacts) }
+ let(:build) { create(:ci_build, :artifacts, pipeline: pipeline) }
it { is_expected.to be_truthy }
context 'is expired' do
- let(:build) { create(:ci_build, :artifacts, :expired) }
+ let(:build) { create(:ci_build, :artifacts, :expired, pipeline: pipeline) }
it { is_expected.to be_falsy }
end
@@ -901,36 +932,32 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
subject(:locked_artifacts) { build.locked_artifacts? }
context 'when pipeline is artifacts_locked' do
- before do
- build.pipeline.artifacts_locked!
- end
+ let(:pipeline) { create(:ci_pipeline, locked: :artifacts_locked) }
context 'artifacts archive does not exist' do
- let(:build) { create(:ci_build) }
+ let(:build) { create(:ci_build, pipeline: pipeline) }
it { is_expected.to be_falsy }
end
context 'artifacts archive exists' do
- let(:build) { create(:ci_build, :artifacts) }
+ let(:build) { create(:ci_build, :artifacts, pipeline: pipeline) }
it { is_expected.to be_truthy }
end
end
context 'when pipeline is unlocked' do
- before do
- build.pipeline.unlocked!
- end
+ let(:pipeline) { create(:ci_pipeline, locked: :unlocked) }
context 'artifacts archive does not exist' do
- let(:build) { create(:ci_build) }
+ let(:build) { create(:ci_build, pipeline: pipeline) }
it { is_expected.to be_falsy }
end
context 'artifacts archive exists' do
- let(:build) { create(:ci_build, :artifacts) }
+ let(:build) { create(:ci_build, :artifacts, pipeline: pipeline) }
it { is_expected.to be_falsy }
end
@@ -938,7 +965,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
describe '#available_artifacts?' do
- let(:build) { create(:ci_build) }
+ let(:build) { create(:ci_build, pipeline: pipeline) }
subject { build.available_artifacts? }
@@ -997,7 +1024,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
subject { build.browsable_artifacts? }
context 'artifacts metadata does exists' do
- let(:build) { create(:ci_build, :artifacts) }
+ let(:build) { create(:ci_build, :artifacts, pipeline: pipeline) }
it { is_expected.to be_truthy }
end
@@ -1007,13 +1034,13 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
subject { build.artifacts_public? }
context 'artifacts with defaults' do
- let(:build) { create(:ci_build, :artifacts) }
+ let(:build) { create(:ci_build, :artifacts, pipeline: pipeline) }
it { is_expected.to be_truthy }
end
context 'non public artifacts' do
- let(:build) { create(:ci_build, :artifacts, :non_public_artifacts) }
+ let(:build) { create(:ci_build, :artifacts, :non_public_artifacts, pipeline: pipeline) }
it { is_expected.to be_falsey }
end
@@ -1047,7 +1074,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
context 'artifacts archive is a zip file and metadata exists' do
- let(:build) { create(:ci_build, :artifacts) }
+ let(:build) { create(:ci_build, :artifacts, pipeline: pipeline) }
it { is_expected.to be_truthy }
end
@@ -1274,12 +1301,12 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
describe '#has_live_trace?' do
subject { build.has_live_trace? }
- let(:build) { create(:ci_build, :trace_live) }
+ let(:build) { create(:ci_build, :trace_live, pipeline: pipeline) }
it { is_expected.to be_truthy }
context 'when build does not have live trace' do
- let(:build) { create(:ci_build) }
+ let(:build) { create(:ci_build, pipeline: pipeline) }
it { is_expected.to be_falsy }
end
@@ -1288,12 +1315,12 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
describe '#has_archived_trace?' do
subject { build.has_archived_trace? }
- let(:build) { create(:ci_build, :trace_artifact) }
+ let(:build) { create(:ci_build, :trace_artifact, pipeline: pipeline) }
it { is_expected.to be_truthy }
context 'when build does not have archived trace' do
- let(:build) { create(:ci_build) }
+ let(:build) { create(:ci_build, pipeline: pipeline) }
it { is_expected.to be_falsy }
end
@@ -1303,7 +1330,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
subject { build.has_job_artifacts? }
context 'when build has a job artifact' do
- let(:build) { create(:ci_build, :artifacts) }
+ let(:build) { create(:ci_build, :artifacts, pipeline: pipeline) }
it { is_expected.to be_truthy }
end
@@ -1313,13 +1340,13 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
subject { build.has_test_reports? }
context 'when build has a test report' do
- let(:build) { create(:ci_build, :test_reports) }
+ let(:build) { create(:ci_build, :test_reports, pipeline: pipeline) }
it { is_expected.to be_truthy }
end
context 'when build does not have a test report' do
- let(:build) { create(:ci_build) }
+ let(:build) { create(:ci_build, pipeline: pipeline) }
it { is_expected.to be_falsey }
end
@@ -1392,7 +1419,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
with_them do
- let(:build) { create(:ci_build, trait, project: project, pipeline: pipeline) }
+ let(:build) { create(:ci_build, trait, pipeline: pipeline) }
let(:event) { state }
context "when transitioning to #{params[:state]}" do
@@ -1416,7 +1443,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
describe 'state transition as a deployable' do
subject { build.send(event) }
- let!(:build) { create(:ci_build, :with_deployment, :start_review_app, project: project, pipeline: pipeline) }
+ let!(:build) { create(:ci_build, :with_deployment, :start_review_app, pipeline: pipeline) }
let(:deployment) { build.deployment }
let(:environment) { deployment.environment }
@@ -1565,7 +1592,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
it 'transitions to running and calls webhook' do
freeze_time do
expect(Deployments::HooksWorker)
- .to receive(:perform_async).with(deployment_id: deployment.id, status: 'running', status_changed_at: Time.current)
+ .to receive(:perform_async).with(hash_including({ 'deployment_id' => deployment.id, 'status' => 'running', 'status_changed_at' => Time.current.to_s }))
subject
end
@@ -1580,7 +1607,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
subject { build.on_stop }
context 'when a job has a specification that it can be stopped from the other job' do
- let(:build) { create(:ci_build, :start_review_app) }
+ let(:build) { create(:ci_build, :start_review_app, pipeline: pipeline) }
it 'returns the other job name' do
is_expected.to eq('stop_review_app')
@@ -1588,7 +1615,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
context 'when a job does not have environment information' do
- let(:build) { create(:ci_build) }
+ let(:build) { create(:ci_build, pipeline: pipeline) }
it 'returns nil' do
is_expected.to be_nil
@@ -1663,7 +1690,8 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
let(:build) do
create(:ci_build,
ref: 'master',
- environment: 'review/$CI_COMMIT_REF_NAME')
+ environment: 'review/$CI_COMMIT_REF_NAME',
+ pipeline: pipeline)
end
it { is_expected.to eq('review/master') }
@@ -1673,7 +1701,8 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
let(:build) do
create(:ci_build,
yaml_variables: [{ key: :APP_HOST, value: 'host' }],
- environment: 'review/$APP_HOST')
+ environment: 'review/$APP_HOST',
+ pipeline: pipeline)
end
it 'returns an expanded environment name with a list of variables' do
@@ -1695,7 +1724,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
context 'when using persisted variables' do
let(:build) do
- create(:ci_build, environment: 'review/x$CI_BUILD_ID')
+ create(:ci_build, environment: 'review/x$CI_BUILD_ID', pipeline: pipeline)
end
it { is_expected.to eq('review/x') }
@@ -1712,7 +1741,8 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
create(:ci_build,
ref: 'master',
yaml_variables: yaml_variables,
- environment: 'review/$ENVIRONMENT_NAME')
+ environment: 'review/$ENVIRONMENT_NAME',
+ pipeline: pipeline)
end
it { is_expected.to eq('review/master') }
@@ -1720,7 +1750,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
describe '#expanded_kubernetes_namespace' do
- let(:build) { create(:ci_build, environment: environment, options: options) }
+ let(:build) { create(:ci_build, environment: environment, options: options, pipeline: pipeline) }
subject { build.expanded_kubernetes_namespace }
@@ -1856,7 +1886,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
context 'build is not erasable' do
- let!(:build) { create(:ci_build) }
+ let!(:build) { create(:ci_build, pipeline: pipeline) }
describe '#erasable?' do
subject { build.erasable? }
@@ -1867,7 +1897,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
context 'build is erasable' do
context 'new artifacts' do
- let!(:build) { create(:ci_build, :test_reports, :trace_artifact, :success, :artifacts) }
+ let!(:build) { create(:ci_build, :test_reports, :trace_artifact, :success, :artifacts, pipeline: pipeline) }
describe '#erasable?' do
subject { build.erasable? }
@@ -1876,7 +1906,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
describe '#erased?' do
- let!(:build) { create(:ci_build, :trace_artifact, :success, :artifacts) }
+ let!(:build) { create(:ci_build, :trace_artifact, :success, :artifacts, pipeline: pipeline) }
subject { build.erased? }
@@ -1970,13 +2000,13 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
context 'when build is created' do
- let(:build) { create(:ci_build, :created) }
+ let(:build) { create(:ci_build, :created, pipeline: pipeline) }
it { is_expected.to be_cancelable }
end
context 'when build is waiting for resource' do
- let(:build) { create(:ci_build, :waiting_for_resource) }
+ let(:build) { create(:ci_build, :waiting_for_resource, pipeline: pipeline) }
it { is_expected.to be_cancelable }
end
@@ -2028,8 +2058,18 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
end
+ describe '#runner_machine' do
+ let_it_be(:runner) { create(:ci_runner) }
+ let_it_be(:runner_machine) { create(:ci_runner_machine, runner: runner) }
+ let_it_be(:build) { create(:ci_build, runner_machine: runner_machine) }
+
+ subject(:build_runner_machine) { described_class.find(build.id).runner_machine }
+
+ it { is_expected.to eq(runner_machine) }
+ end
+
describe '#tag_list' do
- let_it_be(:build) { create(:ci_build, tag_list: ['tag']) }
+ let_it_be(:build) { create(:ci_build, tag_list: ['tag'], pipeline: pipeline) }
context 'when tags are preloaded' do
it 'does not trigger queries' do
@@ -2046,7 +2086,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
describe '#save_tags' do
- let(:build) { create(:ci_build, tag_list: ['tag']) }
+ let(:build) { create(:ci_build, tag_list: ['tag'], pipeline: pipeline) }
it 'saves tags' do
build.save!
@@ -2075,13 +2115,13 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
describe '#has_tags?' do
context 'when build has tags' do
- subject { create(:ci_build, tag_list: ['tag']) }
+ subject { create(:ci_build, tag_list: ['tag'], pipeline: pipeline) }
it { is_expected.to have_tags }
end
context 'when build does not have tags' do
- subject { create(:ci_build, tag_list: []) }
+ subject { create(:ci_build, tag_list: [], pipeline: pipeline) }
it { is_expected.not_to have_tags }
end
@@ -2136,9 +2176,9 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
describe '.keep_artifacts!' do
- let!(:build) { create(:ci_build, artifacts_expire_at: Time.current + 7.days) }
+ let!(:build) { create(:ci_build, artifacts_expire_at: Time.current + 7.days, pipeline: pipeline) }
let!(:builds_for_update) do
- Ci::Build.where(id: create_list(:ci_build, 3, artifacts_expire_at: Time.current + 7.days).map(&:id))
+ Ci::Build.where(id: create_list(:ci_build, 3, artifacts_expire_at: Time.current + 7.days, pipeline: pipeline).map(&:id))
end
it 'resets expire_at' do
@@ -2180,7 +2220,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
describe '#keep_artifacts!' do
- let(:build) { create(:ci_build, artifacts_expire_at: Time.current + 7.days) }
+ let(:build) { create(:ci_build, artifacts_expire_at: Time.current + 7.days, pipeline: pipeline) }
subject { build.keep_artifacts! }
@@ -2202,7 +2242,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
describe '#auto_retry_expected?' do
- subject { create(:ci_build, :failed) }
+ subject { create(:ci_build, :failed, pipeline: pipeline) }
context 'when build is failed and auto retry is configured' do
before do
@@ -2223,20 +2263,20 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
end
- describe '#artifacts_file_for_type' do
- let(:build) { create(:ci_build, :artifacts) }
+ describe '#artifact_for_type' do
+ let(:build) { create(:ci_build) }
+ let!(:archive) { create(:ci_job_artifact, :archive, job: build) }
+ let!(:codequality) { create(:ci_job_artifact, :codequality, job: build) }
let(:file_type) { :archive }
- subject { build.artifacts_file_for_type(file_type) }
-
- it 'queries artifacts for type' do
- expect(build).to receive_message_chain(:job_artifacts, :find_by).with(file_type: [Ci::JobArtifact.file_types[file_type]])
+ subject { build.artifact_for_type(file_type) }
- subject
- end
+ it { is_expected.to eq(archive) }
end
describe '#merge_request' do
+ let_it_be(:merge_request) { create(:merge_request, source_project: project) }
+
subject { pipeline.builds.take.merge_request }
context 'on a branch pipeline' do
@@ -2281,19 +2321,23 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
context 'on a detached merged request pipeline' do
- let(:pipeline) { create(:ci_pipeline, :detached_merge_request_pipeline, :with_job) }
+ let(:pipeline) do
+ create(:ci_pipeline, :detached_merge_request_pipeline, :with_job, merge_request: merge_request)
+ end
it { is_expected.to eq(pipeline.merge_request) }
end
context 'on a legacy detached merged request pipeline' do
- let(:pipeline) { create(:ci_pipeline, :legacy_detached_merge_request_pipeline, :with_job) }
+ let(:pipeline) do
+ create(:ci_pipeline, :legacy_detached_merge_request_pipeline, :with_job, merge_request: merge_request)
+ end
it { is_expected.to eq(pipeline.merge_request) }
end
context 'on a pipeline for merged results' do
- let(:pipeline) { create(:ci_pipeline, :merged_result_pipeline, :with_job) }
+ let(:pipeline) { create(:ci_pipeline, :merged_result_pipeline, :with_job, merge_request: merge_request) }
it { is_expected.to eq(pipeline.merge_request) }
end
@@ -2329,7 +2373,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
context 'when options include artifacts:expose_as' do
- let(:build) { create(:ci_build, options: { artifacts: { expose_as: 'test' } }) }
+ let(:build) { create(:ci_build, options: { artifacts: { expose_as: 'test' } }, pipeline: pipeline) }
it 'saves the presence of expose_as into build metadata' do
expect(build.metadata).to have_exposed_artifacts
@@ -2455,56 +2499,56 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
describe '#playable?' do
context 'when build is a manual action' do
context 'when build has been skipped' do
- subject { build_stubbed(:ci_build, :manual, status: :skipped) }
+ subject { build_stubbed(:ci_build, :manual, status: :skipped, pipeline: pipeline) }
it { is_expected.not_to be_playable }
end
context 'when build has been canceled' do
- subject { build_stubbed(:ci_build, :manual, status: :canceled) }
+ subject { build_stubbed(:ci_build, :manual, status: :canceled, pipeline: pipeline) }
it { is_expected.to be_playable }
end
context 'when build is successful' do
- subject { build_stubbed(:ci_build, :manual, status: :success) }
+ subject { build_stubbed(:ci_build, :manual, status: :success, pipeline: pipeline) }
it { is_expected.to be_playable }
end
context 'when build has failed' do
- subject { build_stubbed(:ci_build, :manual, status: :failed) }
+ subject { build_stubbed(:ci_build, :manual, status: :failed, pipeline: pipeline) }
it { is_expected.to be_playable }
end
context 'when build is a manual untriggered action' do
- subject { build_stubbed(:ci_build, :manual, status: :manual) }
+ subject { build_stubbed(:ci_build, :manual, status: :manual, pipeline: pipeline) }
it { is_expected.to be_playable }
end
context 'when build is a manual and degenerated' do
- subject { build_stubbed(:ci_build, :manual, :degenerated, status: :manual) }
+ subject { build_stubbed(:ci_build, :manual, :degenerated, status: :manual, pipeline: pipeline) }
it { is_expected.not_to be_playable }
end
end
context 'when build is scheduled' do
- subject { build_stubbed(:ci_build, :scheduled) }
+ subject { build_stubbed(:ci_build, :scheduled, pipeline: pipeline) }
it { is_expected.to be_playable }
end
context 'when build is not a manual action' do
- subject { build_stubbed(:ci_build, :success) }
+ subject { build_stubbed(:ci_build, :success, pipeline: pipeline) }
it { is_expected.not_to be_playable }
end
context 'when build is waiting for deployment approval' do
- subject { build_stubbed(:ci_build, :manual, environment: 'production') }
+ subject { build_stubbed(:ci_build, :manual, environment: 'production', pipeline: pipeline) }
before do
create(:deployment, :blocked, deployable: subject)
@@ -2601,7 +2645,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
it { is_expected.to be_truthy }
- context "and there are specific runner" do
+ context "and there is a project runner" do
let!(:runner) { create(:ci_runner, :project, projects: [build.project], contacted_at: 1.second.ago) }
it { is_expected.to be_falsey }
@@ -2855,7 +2899,13 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
before do
allow_next_instance_of(Gitlab::Ci::Variables::Builder) do |builder|
+ pipeline_variables_builder = double(
+ ::Gitlab::Ci::Variables::Builder::Pipeline,
+ predefined_variables: [pipeline_pre_var]
+ )
+
allow(builder).to receive(:predefined_variables) { [build_pre_var] }
+ allow(builder).to receive(:pipeline_variables_builder) { pipeline_variables_builder }
end
allow(build).to receive(:yaml_variables) { [build_yaml_var] }
@@ -2868,9 +2918,6 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
.to receive(:predefined_variables) { [project_pre_var] }
project.variables.create!(key: 'secret', value: 'value')
-
- allow(build.pipeline)
- .to receive(:predefined_variables).and_return([pipeline_pre_var])
end
it 'returns variables in order depending on resource hierarchy' do
@@ -3754,7 +3801,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
describe '#any_unmet_prerequisites?' do
- let(:build) { create(:ci_build, :created) }
+ let(:build) { create(:ci_build, :created, pipeline: pipeline) }
subject { build.any_unmet_prerequisites? }
@@ -3841,7 +3888,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
describe 'state transition: any => [:preparing]' do
- let(:build) { create(:ci_build, :created) }
+ let(:build) { create(:ci_build, :created, pipeline: pipeline) }
before do
allow(build).to receive(:prerequisites).and_return([double])
@@ -3855,7 +3902,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
describe 'when the build is waiting for deployment approval' do
- let(:build) { create(:ci_build, :manual, environment: 'production') }
+ let(:build) { create(:ci_build, :manual, environment: 'production', pipeline: pipeline) }
before do
create(:deployment, :blocked, deployable: build)
@@ -3867,7 +3914,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
describe 'state transition: any => [:pending]' do
- let(:build) { create(:ci_build, :created) }
+ let(:build) { create(:ci_build, :created, pipeline: pipeline) }
it 'queues BuildQueueWorker' do
expect(BuildQueueWorker).to receive(:perform_async).with(build.id)
@@ -3887,8 +3934,10 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
describe 'state transition: pending: :running' do
- let(:runner) { create(:ci_runner) }
- let(:job) { create(:ci_build, :pending, runner: runner) }
+ let_it_be_with_reload(:runner) { create(:ci_runner) }
+ let_it_be_with_reload(:pipeline) { create(:ci_pipeline, project: project) }
+
+ let(:job) { create(:ci_build, :pending, runner: runner, pipeline: pipeline) }
before do
job.project.update_attribute(:build_timeout, 1800)
@@ -3992,7 +4041,9 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
context 'when artifacts of depended job has been erased' do
- let!(:pre_stage_job) { create(:ci_build, :success, pipeline: pipeline, name: 'test', stage_idx: 0, erased_at: 1.minute.ago) }
+ let!(:pre_stage_job) do
+ create(:ci_build, :success, pipeline: pipeline, name: 'test', stage_idx: 0, erased_at: 1.minute.ago)
+ end
it { expect(job).not_to have_valid_build_dependencies }
end
@@ -4049,7 +4100,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
context 'when build is configured to be retried' do
- subject { create(:ci_build, :running, options: { script: ["ls -al"], retry: 3 }, project: project, user: user) }
+ subject { create(:ci_build, :running, options: { script: ["ls -al"], retry: 3 }, pipeline: pipeline, user: user) }
it 'retries build and assigns the same user to it' do
expect_next_instance_of(::Ci::RetryJobService) do |service|
@@ -4098,7 +4149,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
context 'when build is not configured to be retried' do
- subject { create(:ci_build, :running, project: project, user: user, pipeline: pipeline) }
+ subject { create(:ci_build, :running, pipeline: pipeline, user: user) }
let(:pipeline) do
create(:ci_pipeline,
@@ -4162,7 +4213,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
describe '.matches_tag_ids' do
- let_it_be(:build, reload: true) { create(:ci_build, project: project, user: user) }
+ let_it_be(:build, reload: true) { create(:ci_build, pipeline: pipeline, user: user) }
let(:tag_ids) { ::ActsAsTaggableOn::Tag.named_any(tag_list).ids }
@@ -4210,7 +4261,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
describe '.matches_tags' do
- let_it_be(:build, reload: true) { create(:ci_build, project: project, user: user) }
+ let_it_be(:build, reload: true) { create(:ci_build, pipeline: pipeline, user: user) }
subject { described_class.where(id: build).with_any_tags }
@@ -4236,7 +4287,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
describe 'pages deployments' do
- let_it_be(:build, reload: true) { create(:ci_build, project: project, user: user) }
+ let_it_be(:build, reload: true) { create(:ci_build, pipeline: pipeline, user: user) }
context 'when job is "pages"' do
before do
@@ -4562,7 +4613,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
describe '#artifacts_metadata_entry' do
- let_it_be(:build) { create(:ci_build, project: project) }
+ let_it_be(:build) { create(:ci_build, pipeline: pipeline) }
let(:path) { 'other_artifacts_0.1.2/another-subdirectory/banana_sample.gif' }
@@ -4622,7 +4673,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
describe '#publishes_artifacts_reports?' do
- let(:build) { create(:ci_build, options: options) }
+ let(:build) { create(:ci_build, options: options, pipeline: pipeline) }
subject { build.publishes_artifacts_reports? }
@@ -4650,7 +4701,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
describe '#runner_required_feature_names' do
- let(:build) { create(:ci_build, options: options) }
+ let(:build) { create(:ci_build, options: options, pipeline: pipeline) }
subject { build.runner_required_feature_names }
@@ -4672,7 +4723,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
describe '#supported_runner?' do
- let_it_be_with_refind(:build) { create(:ci_build) }
+ let_it_be_with_refind(:build) { create(:ci_build, pipeline: pipeline) }
subject { build.supported_runner?(runner_features) }
@@ -4780,7 +4831,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
context 'when build is a last deployment' do
- let(:build) { create(:ci_build, :success, environment: 'production', pipeline: pipeline, project: project) }
+ let(:build) { create(:ci_build, :success, environment: 'production', pipeline: pipeline) }
let(:environment) { create(:environment, name: 'production', project: build.project) }
let!(:deployment) { create(:deployment, :success, environment: environment, project: environment.project, deployable: build) }
@@ -4788,7 +4839,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
context 'when there is a newer build with deployment' do
- let(:build) { create(:ci_build, :success, environment: 'production', pipeline: pipeline, project: project) }
+ let(:build) { create(:ci_build, :success, environment: 'production', pipeline: pipeline) }
let(:environment) { create(:environment, name: 'production', project: build.project) }
let!(:deployment) { create(:deployment, :success, environment: environment, project: environment.project, deployable: build) }
let!(:last_deployment) { create(:deployment, :success, environment: environment, project: environment.project) }
@@ -4797,7 +4848,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
context 'when build with deployment has failed' do
- let(:build) { create(:ci_build, :failed, environment: 'production', pipeline: pipeline, project: project) }
+ let(:build) { create(:ci_build, :failed, environment: 'production', pipeline: pipeline) }
let(:environment) { create(:environment, name: 'production', project: build.project) }
let!(:deployment) { create(:deployment, :success, environment: environment, project: environment.project, deployable: build) }
@@ -4805,7 +4856,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
context 'when build with deployment is running' do
- let(:build) { create(:ci_build, environment: 'production', pipeline: pipeline, project: project) }
+ let(:build) { create(:ci_build, environment: 'production', pipeline: pipeline) }
let(:environment) { create(:environment, name: 'production', project: build.project) }
let!(:deployment) { create(:deployment, :success, environment: environment, project: environment.project, deployable: build) }
@@ -4815,13 +4866,13 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
describe '#degenerated?' do
context 'when build is degenerated' do
- subject { create(:ci_build, :degenerated) }
+ subject { create(:ci_build, :degenerated, pipeline: pipeline) }
it { is_expected.to be_degenerated }
end
context 'when build is valid' do
- subject { create(:ci_build) }
+ subject { create(:ci_build, pipeline: pipeline) }
it { is_expected.not_to be_degenerated }
@@ -4836,7 +4887,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
describe 'degenerate!' do
- let(:build) { create(:ci_build) }
+ let(:build) { create(:ci_build, pipeline: pipeline) }
subject { build.degenerate! }
@@ -4856,13 +4907,13 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
describe '#archived?' do
context 'when build is degenerated' do
- subject { create(:ci_build, :degenerated) }
+ subject { create(:ci_build, :degenerated, pipeline: pipeline) }
it { is_expected.to be_archived }
end
context 'for old build' do
- subject { create(:ci_build, created_at: 1.day.ago) }
+ subject { create(:ci_build, created_at: 1.day.ago, pipeline: pipeline) }
context 'when archive_builds_in is set' do
before do
@@ -4883,7 +4934,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
describe '#read_metadata_attribute' do
- let(:build) { create(:ci_build, :degenerated) }
+ let(:build) { create(:ci_build, :degenerated, pipeline: pipeline) }
let(:build_options) { { key: "build" } }
let(:metadata_options) { { key: "metadata" } }
let(:default_options) { { key: "default" } }
@@ -4920,7 +4971,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
describe '#write_metadata_attribute' do
- let(:build) { create(:ci_build, :degenerated) }
+ let(:build) { create(:ci_build, :degenerated, pipeline: pipeline) }
let(:options) { { key: "new options" } }
let(:existing_options) { { key: "existing options" } }
@@ -5046,13 +5097,15 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
subject { build.environment_auto_stop_in }
context 'when build option has environment auto_stop_in' do
- let(:build) { create(:ci_build, options: { environment: { name: 'test', auto_stop_in: '1 day' } }) }
+ let(:build) do
+ create(:ci_build, options: { environment: { name: 'test', auto_stop_in: '1 day' } }, pipeline: pipeline)
+ end
it { is_expected.to eq('1 day') }
end
context 'when build option does not have environment auto_stop_in' do
- let(:build) { create(:ci_build) }
+ let(:build) { create(:ci_build, pipeline: pipeline) }
it { is_expected.to be_nil }
end
@@ -5372,7 +5425,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
describe '.build_matchers' do
- let_it_be(:pipeline) { create(:ci_pipeline, :protected) }
+ let_it_be(:pipeline) { create(:ci_pipeline, :protected, project: project) }
subject(:matchers) { pipeline.builds.build_matchers(pipeline.project) }
@@ -5421,7 +5474,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
describe '#build_matcher' do
let_it_be(:build) do
- build_stubbed(:ci_build, tag_list: %w[tag1 tag2])
+ build_stubbed(:ci_build, tag_list: %w[tag1 tag2], pipeline: pipeline)
end
subject(:matcher) { build.build_matcher }
@@ -5557,7 +5610,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
it 'does not generate cross DB queries when a record is created via FactoryBot' do
with_cross_database_modification_prevented do
- create(:ci_build)
+ create(:ci_build, pipeline: pipeline)
end
end
@@ -5585,7 +5638,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
it_behaves_like 'cleanup by a loose foreign key' do
- let!(:model) { create(:ci_build, user: create(:user)) }
+ let!(:model) { create(:ci_build, user: create(:user), pipeline: pipeline) }
let!(:parent) { model.user }
end
@@ -5595,7 +5648,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
context 'when given new job variables' do
context 'when the cloned build has an action' do
it 'applies the new job variables' do
- build = create(:ci_build, :actionable)
+ build = create(:ci_build, :actionable, pipeline: pipeline)
create(:ci_job_variable, job: build, key: 'TEST_KEY', value: 'old value')
create(:ci_job_variable, job: build, key: 'OLD_KEY', value: 'i will not live for long')
@@ -5614,7 +5667,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
context 'when the cloned build does not have an action' do
it 'applies the old job variables' do
- build = create(:ci_build)
+ build = create(:ci_build, pipeline: pipeline)
create(:ci_job_variable, job: build, key: 'TEST_KEY', value: 'old value')
new_build = build.clone(
@@ -5632,7 +5685,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
context 'when not given new job variables' do
it 'applies the old job variables' do
- build = create(:ci_build)
+ build = create(:ci_build, pipeline: pipeline)
create(:ci_job_variable, job: build, key: 'TEST_KEY', value: 'old value')
new_build = build.clone(current_user: user)
@@ -5646,14 +5699,14 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
describe '#test_suite_name' do
- let(:build) { create(:ci_build, name: 'test') }
+ let(:build) { create(:ci_build, name: 'test', pipeline: pipeline) }
it 'uses the group name for test suite name' do
expect(build.test_suite_name).to eq('test')
end
context 'when build is part of parallel build' do
- let(:build) { create(:ci_build, name: 'build 1/2') }
+ let(:build) { create(:ci_build, name: 'build 1/2', pipeline: pipeline) }
it 'uses the group name for test suite name' do
expect(build.test_suite_name).to eq('build')
@@ -5661,7 +5714,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
end
context 'when build is part of matrix build' do
- let!(:matrix_build) { create(:ci_build, :matrix) }
+ let!(:matrix_build) { create(:ci_build, :matrix, pipeline: pipeline) }
it 'uses the job name for the test suite' do
expect(matrix_build.test_suite_name).to eq(matrix_build.name)
@@ -5672,7 +5725,8 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
describe '#runtime_hooks' do
let(:build1) do
FactoryBot.build(:ci_build,
- options: { hooks: { pre_get_sources_script: ["echo 'hello pre_get_sources_script'"] } })
+ options: { hooks: { pre_get_sources_script: ["echo 'hello pre_get_sources_script'"] } },
+ pipeline: pipeline)
end
subject(:runtime_hooks) { build1.runtime_hooks }
@@ -5687,7 +5741,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
describe 'partitioning', :ci_partitionable do
include Ci::PartitioningHelpers
- let(:new_pipeline) { create(:ci_pipeline) }
+ let(:new_pipeline) { create(:ci_pipeline, project: project) }
let(:ci_build) { FactoryBot.build(:ci_build, pipeline: new_pipeline) }
before do
@@ -5711,7 +5765,7 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
describe 'assigning token', :ci_partitionable do
include Ci::PartitioningHelpers
- let(:new_pipeline) { create(:ci_pipeline) }
+ let(:new_pipeline) { create(:ci_pipeline, project: project) }
let(:ci_build) { create(:ci_build, pipeline: new_pipeline) }
before do
@@ -5741,4 +5795,118 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration do
expect { build.remove_token! }.not_to change(build, :token)
end
end
+
+ describe 'metadata partitioning', :ci_partitioning do
+ let(:pipeline) { create(:ci_pipeline, project: project, partition_id: ci_testing_partition_id) }
+
+ let(:build) do
+ FactoryBot.build(:ci_build, pipeline: pipeline)
+ end
+
+ it 'creates the metadata record and assigns its partition' do
+ # The record is initialized by the factory calling metadatable setters
+ build.metadata = nil
+
+ expect(build.metadata).to be_nil
+
+ expect(build.save!).to be_truthy
+
+ expect(build.metadata).to be_present
+ expect(build.metadata).to be_valid
+ expect(build.metadata.partition_id).to eq(ci_testing_partition_id)
+ end
+ end
+
+ describe 'secrets management id_tokens usage data' do
+ context 'when ID tokens are defined' do
+ context 'on create' do
+ let(:ci_build) { FactoryBot.build(:ci_build, user: user, id_tokens: { 'ID_TOKEN_1' => { aud: 'developers' } }) }
+
+ it 'tracks RedisHLL event with user_id' do
+ expect(::Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:track_event)
+ .with('i_ci_secrets_management_id_tokens_build_created', values: user.id)
+
+ ci_build.save!
+ end
+
+ it 'tracks Snowplow event with RedisHLL context' do
+ params = {
+ category: described_class.to_s,
+ action: 'create_id_tokens',
+ namespace: ci_build.namespace,
+ user: user,
+ label: 'redis_hll_counters.ci_secrets_management.i_ci_secrets_management_id_tokens_build_created_monthly',
+ ultimate_namespace_id: ci_build.namespace.root_ancestor.id,
+ context: [Gitlab::Tracking::ServicePingContext.new(
+ data_source: :redis_hll,
+ event: 'i_ci_secrets_management_id_tokens_build_created'
+ ).to_context.to_json]
+ }
+
+ ci_build.save!
+ expect_snowplow_event(**params)
+ end
+ end
+
+ context 'on update' do
+ let_it_be(:ci_build) { create(:ci_build, user: user, id_tokens: { 'ID_TOKEN_1' => { aud: 'developers' } }) }
+
+ it 'does not track RedisHLL event' do
+ expect(Gitlab::UsageDataCounters::HLLRedisCounter).not_to receive(:track_event)
+
+ ci_build.success
+ end
+
+ it 'does not track Snowplow event' do
+ ci_build.success
+
+ expect_no_snowplow_event
+ end
+ end
+ end
+
+ context 'when ID tokens are not defined' do
+ let(:ci_build) { FactoryBot.build(:ci_build, user: user) }
+
+ context 'on create' do
+ it 'does not track RedisHLL event' do
+ expect(Gitlab::UsageDataCounters::HLLRedisCounter).not_to receive(:track_event)
+
+ ci_build.save!
+ end
+
+ it 'does not track Snowplow event' do
+ ci_build.save!
+ expect_no_snowplow_event
+ end
+ end
+ end
+ end
+
+ describe 'job artifact associations' do
+ Ci::JobArtifact.file_types.each do |type, _|
+ method = "job_artifacts_#{type}"
+
+ describe "##{method}" do
+ subject { build.send(method) }
+
+ context "when job has an artifact of type #{type}" do
+ let!(:artifact) do
+ create(
+ :ci_job_artifact,
+ job: build,
+ file_type: type,
+ file_format: Ci::JobArtifact::TYPE_AND_FORMAT_PAIRS[type.to_sym]
+ )
+ end
+
+ it { is_expected.to eq(artifact) }
+ end
+
+ context "when job has no artifact of type #{type}" do
+ it { is_expected.to be_nil }
+ end
+ end
+ end
+ end
end
diff --git a/spec/models/ci/group_variable_spec.rb b/spec/models/ci/group_variable_spec.rb
index fc5a9c879f6..e73319cfcd7 100644
--- a/spec/models/ci/group_variable_spec.rb
+++ b/spec/models/ci/group_variable_spec.rb
@@ -2,10 +2,13 @@
require 'spec_helper'
-RSpec.describe Ci::GroupVariable do
- subject { build(:ci_group_variable) }
+RSpec.describe Ci::GroupVariable, feature_category: :pipeline_authoring do
+ let_it_be_with_refind(:group) { create(:group) }
+
+ subject { build(:ci_group_variable, group: group) }
it_behaves_like "CI variable"
+ it_behaves_like 'includes Limitable concern'
it { is_expected.to include_module(Presentable) }
it { is_expected.to include_module(Ci::Maskable) }
diff --git a/spec/models/ci/job_artifact_spec.rb b/spec/models/ci/job_artifact_spec.rb
index a1fd51f60ea..e94445f17cd 100644
--- a/spec/models/ci/job_artifact_spec.rb
+++ b/spec/models/ci/job_artifact_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::JobArtifact do
+RSpec.describe Ci::JobArtifact, feature_category: :build_artifacts do
let(:artifact) { create(:ci_job_artifact, :archive) }
describe "Associations" do
@@ -27,6 +27,29 @@ RSpec.describe Ci::JobArtifact do
subject { build(:ci_job_artifact, :archive, job: job, size: 107464) }
end
+ describe 'after_create_commit callback' do
+ it 'logs the job artifact create' do
+ artifact = build(:ci_job_artifact, file_type: 3, size: 8888, file_format: 2, locked: 1)
+
+ expect(Gitlab::Ci::Artifacts::Logger).to receive(:log_created) do |record|
+ expect(record.size).to eq(artifact.size)
+ expect(record.file_type).to eq(artifact.file_type)
+ expect(record.file_format).to eq(artifact.file_format)
+ expect(record.locked).to eq(artifact.locked)
+ end
+
+ artifact.save!
+ end
+ end
+
+ describe 'after_destroy_commit callback' do
+ it 'logs the job artifact destroy' do
+ expect(Gitlab::Ci::Artifacts::Logger).to receive(:log_deleted).with(artifact, :log_destroy)
+
+ artifact.destroy!
+ end
+ end
+
describe '.not_expired' do
it 'returns artifacts that have not expired' do
_expired_artifact = create(:ci_job_artifact, :expired)
@@ -770,4 +793,10 @@ RSpec.describe Ci::JobArtifact do
end
end
end
+
+ describe '#filename' do
+ subject { artifact.filename }
+
+ it { is_expected.to eq(artifact.file.filename) }
+ end
end
diff --git a/spec/models/ci/job_token/allowlist_spec.rb b/spec/models/ci/job_token/allowlist_spec.rb
index 45083d64393..3a2673c7c26 100644
--- a/spec/models/ci/job_token/allowlist_spec.rb
+++ b/spec/models/ci/job_token/allowlist_spec.rb
@@ -3,6 +3,7 @@
require 'spec_helper'
RSpec.describe Ci::JobToken::Allowlist, feature_category: :continuous_integration do
+ include Ci::JobTokenScopeHelpers
using RSpec::Parameterized::TableSyntax
let_it_be(:source_project) { create(:project) }
@@ -24,11 +25,11 @@ RSpec.describe Ci::JobToken::Allowlist, feature_category: :continuous_integratio
end
context 'when projects are added to the scope' do
- include_context 'with scoped projects'
+ include_context 'with a project in each allowlist'
where(:direction, :additional_project) do
- :outbound | ref(:outbound_scoped_project)
- :inbound | ref(:inbound_scoped_project)
+ :outbound | ref(:outbound_allowlist_project)
+ :inbound | ref(:inbound_allowlist_project)
end
with_them do
@@ -39,6 +40,26 @@ RSpec.describe Ci::JobToken::Allowlist, feature_category: :continuous_integratio
end
end
+ describe 'add!' do
+ let_it_be(:added_project) { create(:project) }
+ let_it_be(:user) { create(:user) }
+
+ subject { allowlist.add!(added_project, user: user) }
+
+ [:inbound, :outbound].each do |d|
+ let(:direction) { d }
+
+ it 'adds the project' do
+ subject
+
+ expect(allowlist.projects).to contain_exactly(source_project, added_project)
+ expect(subject.added_by_id).to eq(user.id)
+ expect(subject.source_project_id).to eq(source_project.id)
+ expect(subject.target_project_id).to eq(added_project.id)
+ end
+ end
+ end
+
describe '#includes?' do
subject { allowlist.includes?(includes_project) }
@@ -57,16 +78,16 @@ RSpec.describe Ci::JobToken::Allowlist, feature_category: :continuous_integratio
end
end
- context 'with scoped projects' do
- include_context 'with scoped projects'
+ context 'with a project in each allowlist' do
+ include_context 'with a project in each allowlist'
where(:includes_project, :direction, :result) do
ref(:source_project) | :outbound | false
ref(:source_project) | :inbound | false
- ref(:inbound_scoped_project) | :outbound | false
- ref(:inbound_scoped_project) | :inbound | true
- ref(:outbound_scoped_project) | :outbound | true
- ref(:outbound_scoped_project) | :inbound | false
+ ref(:inbound_allowlist_project) | :outbound | false
+ ref(:inbound_allowlist_project) | :inbound | true
+ ref(:outbound_allowlist_project) | :outbound | true
+ ref(:outbound_allowlist_project) | :inbound | false
ref(:unscoped_project1) | :outbound | false
ref(:unscoped_project1) | :inbound | false
ref(:unscoped_project2) | :outbound | false
diff --git a/spec/models/ci/job_token/project_scope_link_spec.rb b/spec/models/ci/job_token/project_scope_link_spec.rb
index 91491733c44..310f9b550f4 100644
--- a/spec/models/ci/job_token/project_scope_link_spec.rb
+++ b/spec/models/ci/job_token/project_scope_link_spec.rb
@@ -18,15 +18,40 @@ RSpec.describe Ci::JobToken::ProjectScopeLink, feature_category: :continuous_int
describe 'unique index' do
let!(:link) { create(:ci_job_token_project_scope_link) }
- it 'raises an error' do
+ it 'raises an error, when not unique' do
expect do
create(:ci_job_token_project_scope_link,
source_project: link.source_project,
- target_project: link.target_project)
+ target_project: link.target_project,
+ direction: link.direction)
end.to raise_error(ActiveRecord::RecordNotUnique)
end
end
+ describe '.create' do
+ let_it_be(:target) { create(:project) }
+ let(:new_link) { described_class.create(source_project: project, target_project: target) } # rubocop:disable Rails/SaveBang
+
+ context 'when there are more than PROJECT_LINK_DIRECTIONAL_LIMIT existing links' do
+ before do
+ create_list(:ci_job_token_project_scope_link, 5, source_project: project)
+ stub_const("#{described_class}::PROJECT_LINK_DIRECTIONAL_LIMIT", 3)
+ end
+
+ it 'invalidates new links and prevents them from being created' do
+ expect { new_link }.not_to change { described_class.count }
+ expect(new_link).not_to be_persisted
+ expect(new_link.errors.full_messages)
+ .to include('Source project exceeds the allowable number of project links in this direction')
+ end
+
+ it 'does not invalidate existing links' do
+ expect(described_class.count).to be > described_class::PROJECT_LINK_DIRECTIONAL_LIMIT
+ expect(described_class.all).to all(be_valid)
+ end
+ end
+ end
+
describe 'validations' do
it 'must have a source project', :aggregate_failures do
link = build(:ci_job_token_project_scope_link, source_project: nil)
diff --git a/spec/models/ci/job_token/scope_spec.rb b/spec/models/ci/job_token/scope_spec.rb
index 37c56973506..9ae061a3702 100644
--- a/spec/models/ci/job_token/scope_spec.rb
+++ b/spec/models/ci/job_token/scope_spec.rb
@@ -2,78 +2,171 @@
require 'spec_helper'
-RSpec.describe Ci::JobToken::Scope, feature_category: :continuous_integration do
- let_it_be(:source_project) { create(:project, ci_outbound_job_token_scope_enabled: true) }
+RSpec.describe Ci::JobToken::Scope, feature_category: :continuous_integration, factory_default: :keep do
+ include Ci::JobTokenScopeHelpers
+ using RSpec::Parameterized::TableSyntax
+
+ let_it_be(:project) { create_default(:project) }
+ let_it_be(:user) { create_default(:user) }
+ let_it_be(:namespace) { create_default(:namespace) }
+
+ let_it_be(:source_project) do
+ create(:project,
+ ci_outbound_job_token_scope_enabled: true,
+ ci_inbound_job_token_scope_enabled: true
+ )
+ end
+
+ let(:current_project) { source_project }
- let(:scope) { described_class.new(source_project) }
+ let(:scope) { described_class.new(current_project) }
- describe '#all_projects' do
- subject(:all_projects) { scope.all_projects }
+ describe '#outbound_projects' do
+ subject { scope.outbound_projects }
context 'when no projects are added to the scope' do
it 'returns the project defining the scope' do
- expect(all_projects).to contain_exactly(source_project)
+ expect(subject).to contain_exactly(current_project)
end
end
context 'when projects are added to the scope' do
- include_context 'with scoped projects'
+ include_context 'with accessible and inaccessible projects'
it 'returns all projects that can be accessed from a given scope' do
- expect(subject).to contain_exactly(source_project, outbound_scoped_project)
+ expect(subject).to contain_exactly(current_project, outbound_allowlist_project, fully_accessible_project)
end
end
end
- describe '#allows?' do
- subject { scope.allows?(includes_project) }
+ describe '#inbound_projects' do
+ subject { scope.inbound_projects }
- context 'without scoped projects' do
- context 'when self referential' do
- let(:includes_project) { source_project }
+ context 'when no projects are added to the scope' do
+ it 'returns the project defining the scope' do
+ expect(subject).to contain_exactly(current_project)
+ end
+ end
+
+ context 'when projects are added to the scope' do
+ include_context 'with accessible and inaccessible projects'
- it { is_expected.to be_truthy }
+ it 'returns all projects that can be accessed from a given scope' do
+ expect(subject).to contain_exactly(current_project, inbound_allowlist_project)
end
end
+ end
+
+ describe 'add!' do
+ let_it_be(:new_project) { create(:project) }
- context 'with scoped projects' do
- include_context 'with scoped projects'
+ subject { scope.add!(new_project, direction: direction, user: user) }
- context 'when project is in outbound scope' do
- let(:includes_project) { outbound_scoped_project }
+ [:inbound, :outbound].each do |d|
+ let(:direction) { d }
- it { is_expected.to be_truthy }
+ it 'adds the project' do
+ subject
+
+ expect(scope.send("#{direction}_projects")).to contain_exactly(current_project, new_project)
end
+ end
- context 'when project is in inbound scope' do
- let(:includes_project) { inbound_scoped_project }
+ # Context and before block can go away leaving just the example in 16.0
+ context 'with inbound only enabled' do
+ before do
+ project.ci_cd_settings.update!(job_token_scope_enabled: false)
+ end
- it { is_expected.to be_falsey }
+ it 'provides access' do
+ expect do
+ scope.add!(new_project, direction: :inbound, user: user)
+ end.to change { described_class.new(new_project).accessible?(current_project) }.from(false).to(true)
end
+ end
+ end
+
+ RSpec.shared_examples 'enforces outbound scope only' do
+ include_context 'with accessible and inaccessible projects'
+
+ where(:accessed_project, :result) do
+ ref(:current_project) | true
+ ref(:inbound_allowlist_project) | false
+ ref(:unscoped_project1) | false
+ ref(:unscoped_project2) | false
+ ref(:outbound_allowlist_project) | true
+ ref(:inbound_accessible_project) | false
+ ref(:fully_accessible_project) | true
+ end
- context 'when project is linked to a different project' do
- let(:includes_project) { unscoped_project1 }
+ with_them do
+ it { is_expected.to eq(result) }
+ end
+ end
+
+ describe 'accessible?' do
+ subject { scope.accessible?(accessed_project) }
+
+ context 'with inbound and outbound scopes enabled' do
+ context 'when inbound and outbound access setup' do
+ include_context 'with accessible and inaccessible projects'
+
+ where(:accessed_project, :result) do
+ ref(:current_project) | true
+ ref(:inbound_allowlist_project) | false
+ ref(:unscoped_project1) | false
+ ref(:unscoped_project2) | false
+ ref(:outbound_allowlist_project) | false
+ ref(:inbound_accessible_project) | false
+ ref(:fully_accessible_project) | true
+ end
+
+ with_them do
+ it 'allows self and projects allowed from both directions' do
+ is_expected.to eq(result)
+ end
+ end
+ end
+ end
- it { is_expected.to be_falsey }
+ context 'with inbound scope enabled and outbound scope disabled' do
+ before do
+ accessed_project.update!(ci_inbound_job_token_scope_enabled: true)
+ current_project.update!(ci_outbound_job_token_scope_enabled: false)
end
- context 'when project is unlinked to a project' do
- let(:includes_project) { unscoped_project2 }
+ include_context 'with accessible and inaccessible projects'
- it { is_expected.to be_falsey }
+ where(:accessed_project, :result) do
+ ref(:current_project) | true
+ ref(:inbound_allowlist_project) | false
+ ref(:unscoped_project1) | false
+ ref(:unscoped_project2) | false
+ ref(:outbound_allowlist_project) | false
+ ref(:inbound_accessible_project) | true
+ ref(:fully_accessible_project) | true
end
- context 'when project scope setting is disabled' do
- let(:includes_project) { unscoped_project1 }
+ with_them do
+ it { is_expected.to eq(result) }
+ end
+ end
- before do
- source_project.ci_outbound_job_token_scope_enabled = false
- end
+ context 'with inbound scope disabled and outbound scope enabled' do
+ before do
+ accessed_project.update!(ci_inbound_job_token_scope_enabled: false)
+ current_project.update!(ci_outbound_job_token_scope_enabled: true)
+ end
- it 'considers any project to be part of the scope' do
- expect(subject).to be_truthy
- end
+ include_examples 'enforces outbound scope only'
+ end
+
+ context 'when inbound scope flag disabled' do
+ before do
+ stub_feature_flags(ci_inbound_job_token_scope: false)
end
+
+ include_examples 'enforces outbound scope only'
end
end
end
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 5888f9d109c..61422978df7 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -226,9 +226,9 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep, feature_category:
let_it_be(:pipeline2) { create(:ci_pipeline, name: 'Chatops pipeline') }
context 'when name exists' do
- let(:name) { 'build Pipeline' }
+ let(:name) { 'Build pipeline' }
- it 'performs case insensitive compare' do
+ it 'performs exact compare' do
is_expected.to contain_exactly(pipeline1)
end
end
@@ -1070,296 +1070,6 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep, feature_category:
end
end
- describe '#predefined_variables' do
- subject { pipeline.predefined_variables }
-
- let(:pipeline) { build(:ci_empty_pipeline, :created) }
-
- it 'includes all predefined variables in a valid order' do
- keys = subject.map { |variable| variable[:key] }
-
- expect(keys).to eq %w[
- CI_PIPELINE_IID
- CI_PIPELINE_SOURCE
- CI_PIPELINE_CREATED_AT
- CI_COMMIT_SHA
- CI_COMMIT_SHORT_SHA
- CI_COMMIT_BEFORE_SHA
- CI_COMMIT_REF_NAME
- CI_COMMIT_REF_SLUG
- CI_COMMIT_BRANCH
- CI_COMMIT_MESSAGE
- CI_COMMIT_TITLE
- CI_COMMIT_DESCRIPTION
- CI_COMMIT_REF_PROTECTED
- CI_COMMIT_TIMESTAMP
- CI_COMMIT_AUTHOR
- CI_BUILD_REF
- CI_BUILD_BEFORE_SHA
- CI_BUILD_REF_NAME
- CI_BUILD_REF_SLUG
- ]
- end
-
- context 'when merge request is present' do
- let_it_be(:assignees) { create_list(:user, 2) }
- let_it_be(:milestone) { create(:milestone, project: project) }
- let_it_be(:labels) { create_list(:label, 2) }
-
- let(:merge_request) do
- create(:merge_request, :simple,
- source_project: project,
- target_project: project,
- assignees: assignees,
- milestone: milestone,
- labels: labels)
- end
-
- context 'when pipeline for merge request is created' do
- let(:pipeline) do
- create(:ci_pipeline, :detached_merge_request_pipeline,
- ci_ref_presence: false,
- user: user,
- merge_request: merge_request)
- end
-
- before do
- project.add_developer(user)
- end
-
- it 'exposes merge request pipeline variables' do
- expect(subject.to_hash)
- .to include(
- 'CI_MERGE_REQUEST_ID' => merge_request.id.to_s,
- 'CI_MERGE_REQUEST_IID' => merge_request.iid.to_s,
- 'CI_MERGE_REQUEST_REF_PATH' => merge_request.ref_path.to_s,
- 'CI_MERGE_REQUEST_PROJECT_ID' => merge_request.project.id.to_s,
- 'CI_MERGE_REQUEST_PROJECT_PATH' => merge_request.project.full_path,
- 'CI_MERGE_REQUEST_PROJECT_URL' => merge_request.project.web_url,
- 'CI_MERGE_REQUEST_TARGET_BRANCH_NAME' => merge_request.target_branch.to_s,
- 'CI_MERGE_REQUEST_TARGET_BRANCH_PROTECTED' => ProtectedBranch.protected?(merge_request.target_project, merge_request.target_branch).to_s,
- 'CI_MERGE_REQUEST_TARGET_BRANCH_SHA' => '',
- 'CI_MERGE_REQUEST_SOURCE_PROJECT_ID' => merge_request.source_project.id.to_s,
- 'CI_MERGE_REQUEST_SOURCE_PROJECT_PATH' => merge_request.source_project.full_path,
- 'CI_MERGE_REQUEST_SOURCE_PROJECT_URL' => merge_request.source_project.web_url,
- 'CI_MERGE_REQUEST_SOURCE_BRANCH_NAME' => merge_request.source_branch.to_s,
- 'CI_MERGE_REQUEST_SOURCE_BRANCH_SHA' => '',
- 'CI_MERGE_REQUEST_TITLE' => merge_request.title,
- 'CI_MERGE_REQUEST_ASSIGNEES' => merge_request.assignee_username_list,
- 'CI_MERGE_REQUEST_MILESTONE' => milestone.title,
- 'CI_MERGE_REQUEST_LABELS' => labels.map(&:title).sort.join(','),
- 'CI_MERGE_REQUEST_EVENT_TYPE' => 'detached',
- 'CI_OPEN_MERGE_REQUESTS' => merge_request.to_reference(full: true))
- end
-
- it 'exposes diff variables' do
- expect(subject.to_hash)
- .to include(
- 'CI_MERGE_REQUEST_DIFF_ID' => merge_request.merge_request_diff.id.to_s,
- 'CI_MERGE_REQUEST_DIFF_BASE_SHA' => merge_request.merge_request_diff.base_commit_sha)
- end
-
- context 'without assignee' do
- let(:assignees) { [] }
-
- it 'does not expose assignee variable' do
- expect(subject.to_hash.keys).not_to include('CI_MERGE_REQUEST_ASSIGNEES')
- end
- end
-
- context 'without milestone' do
- let(:milestone) { nil }
-
- it 'does not expose milestone variable' do
- expect(subject.to_hash.keys).not_to include('CI_MERGE_REQUEST_MILESTONE')
- end
- end
-
- context 'without labels' do
- let(:labels) { [] }
-
- it 'does not expose labels variable' do
- expect(subject.to_hash.keys).not_to include('CI_MERGE_REQUEST_LABELS')
- end
- end
- end
-
- context 'when pipeline on branch is created' do
- let(:pipeline) do
- create(:ci_pipeline, project: project, user: user, ref: 'feature')
- end
-
- context 'when a merge request is created' do
- before do
- merge_request
- end
-
- context 'when user has access to project' do
- before do
- project.add_developer(user)
- end
-
- it 'merge request references are returned matching the pipeline' do
- expect(subject.to_hash).to include(
- 'CI_OPEN_MERGE_REQUESTS' => merge_request.to_reference(full: true))
- end
- end
-
- context 'when user does not have access to project' do
- it 'CI_OPEN_MERGE_REQUESTS is not returned' do
- expect(subject.to_hash).not_to have_key('CI_OPEN_MERGE_REQUESTS')
- end
- end
- end
-
- context 'when no a merge request is created' do
- it 'CI_OPEN_MERGE_REQUESTS is not returned' do
- expect(subject.to_hash).not_to have_key('CI_OPEN_MERGE_REQUESTS')
- end
- end
- end
-
- context 'with merged results' do
- let(:pipeline) do
- create(:ci_pipeline, :merged_result_pipeline, merge_request: merge_request)
- end
-
- it 'exposes merge request pipeline variables' do
- expect(subject.to_hash)
- .to include(
- 'CI_MERGE_REQUEST_ID' => merge_request.id.to_s,
- 'CI_MERGE_REQUEST_IID' => merge_request.iid.to_s,
- 'CI_MERGE_REQUEST_REF_PATH' => merge_request.ref_path.to_s,
- 'CI_MERGE_REQUEST_PROJECT_ID' => merge_request.project.id.to_s,
- 'CI_MERGE_REQUEST_PROJECT_PATH' => merge_request.project.full_path,
- 'CI_MERGE_REQUEST_PROJECT_URL' => merge_request.project.web_url,
- 'CI_MERGE_REQUEST_TARGET_BRANCH_NAME' => merge_request.target_branch.to_s,
- 'CI_MERGE_REQUEST_TARGET_BRANCH_PROTECTED' => ProtectedBranch.protected?(merge_request.target_project, merge_request.target_branch).to_s,
- 'CI_MERGE_REQUEST_TARGET_BRANCH_SHA' => merge_request.target_branch_sha,
- 'CI_MERGE_REQUEST_SOURCE_PROJECT_ID' => merge_request.source_project.id.to_s,
- 'CI_MERGE_REQUEST_SOURCE_PROJECT_PATH' => merge_request.source_project.full_path,
- 'CI_MERGE_REQUEST_SOURCE_PROJECT_URL' => merge_request.source_project.web_url,
- 'CI_MERGE_REQUEST_SOURCE_BRANCH_NAME' => merge_request.source_branch.to_s,
- 'CI_MERGE_REQUEST_SOURCE_BRANCH_SHA' => merge_request.source_branch_sha,
- 'CI_MERGE_REQUEST_TITLE' => merge_request.title,
- 'CI_MERGE_REQUEST_ASSIGNEES' => merge_request.assignee_username_list,
- 'CI_MERGE_REQUEST_MILESTONE' => milestone.title,
- 'CI_MERGE_REQUEST_LABELS' => labels.map(&:title).sort.join(','),
- 'CI_MERGE_REQUEST_EVENT_TYPE' => 'merged_result')
- end
-
- it 'exposes diff variables' do
- expect(subject.to_hash)
- .to include(
- 'CI_MERGE_REQUEST_DIFF_ID' => merge_request.merge_request_diff.id.to_s,
- 'CI_MERGE_REQUEST_DIFF_BASE_SHA' => merge_request.merge_request_diff.base_commit_sha)
- end
- end
- end
-
- context 'when source is external pull request' do
- let(:pipeline) do
- create(:ci_pipeline, source: :external_pull_request_event, external_pull_request: pull_request)
- end
-
- let(:pull_request) { create(:external_pull_request, project: project) }
-
- it 'exposes external pull request pipeline variables' do
- expect(subject.to_hash)
- .to include(
- 'CI_EXTERNAL_PULL_REQUEST_IID' => pull_request.pull_request_iid.to_s,
- 'CI_EXTERNAL_PULL_REQUEST_SOURCE_REPOSITORY' => pull_request.source_repository,
- 'CI_EXTERNAL_PULL_REQUEST_TARGET_REPOSITORY' => pull_request.target_repository,
- 'CI_EXTERNAL_PULL_REQUEST_SOURCE_BRANCH_SHA' => pull_request.source_sha,
- 'CI_EXTERNAL_PULL_REQUEST_TARGET_BRANCH_SHA' => pull_request.target_sha,
- 'CI_EXTERNAL_PULL_REQUEST_SOURCE_BRANCH_NAME' => pull_request.source_branch,
- 'CI_EXTERNAL_PULL_REQUEST_TARGET_BRANCH_NAME' => pull_request.target_branch
- )
- end
- end
-
- describe 'variable CI_KUBERNETES_ACTIVE' do
- context 'when pipeline.has_kubernetes_active? is true' do
- before do
- allow(pipeline).to receive(:has_kubernetes_active?).and_return(true)
- end
-
- it "is included with value 'true'" do
- expect(subject.to_hash).to include('CI_KUBERNETES_ACTIVE' => 'true')
- end
- end
-
- context 'when pipeline.has_kubernetes_active? is false' do
- before do
- allow(pipeline).to receive(:has_kubernetes_active?).and_return(false)
- end
-
- it 'is not included' do
- expect(subject.to_hash).not_to have_key('CI_KUBERNETES_ACTIVE')
- end
- end
- end
-
- describe 'variable CI_GITLAB_FIPS_MODE' do
- context 'when FIPS flag is enabled' do
- before do
- allow(Gitlab::FIPS).to receive(:enabled?).and_return(true)
- end
-
- it "is included with value 'true'" do
- expect(subject.to_hash).to include('CI_GITLAB_FIPS_MODE' => 'true')
- end
- end
-
- context 'when FIPS flag is disabled' do
- before do
- allow(Gitlab::FIPS).to receive(:enabled?).and_return(false)
- end
-
- it 'is not included' do
- expect(subject.to_hash).not_to have_key('CI_GITLAB_FIPS_MODE')
- end
- end
- end
-
- context 'when tag is not found' do
- let(:pipeline) do
- create(:ci_pipeline, project: project, ref: 'not_found_tag', tag: true)
- end
-
- it 'does not expose tag variables' do
- expect(subject.to_hash.keys)
- .not_to include(
- 'CI_COMMIT_TAG',
- 'CI_COMMIT_TAG_MESSAGE',
- 'CI_BUILD_TAG'
- )
- end
- end
-
- context 'without a commit' do
- let(:pipeline) { build(:ci_empty_pipeline, :created, sha: nil) }
-
- it 'does not expose commit variables' do
- expect(subject.to_hash.keys)
- .not_to include(
- 'CI_COMMIT_SHA',
- 'CI_COMMIT_SHORT_SHA',
- 'CI_COMMIT_BEFORE_SHA',
- 'CI_COMMIT_REF_NAME',
- 'CI_COMMIT_REF_SLUG',
- 'CI_COMMIT_BRANCH',
- 'CI_COMMIT_TAG',
- 'CI_COMMIT_MESSAGE',
- 'CI_COMMIT_TITLE',
- 'CI_COMMIT_DESCRIPTION',
- 'CI_COMMIT_REF_PROTECTED',
- 'CI_COMMIT_TIMESTAMP',
- 'CI_COMMIT_AUTHOR')
- end
- end
- end
-
describe '#protected_ref?' do
let(:pipeline) { build(:ci_empty_pipeline, :created) }
@@ -5664,6 +5374,34 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep, feature_category:
end
end
+ describe '#merge_request_diff' do
+ context 'when the pipeline has no merge request' do
+ it 'is nil' do
+ pipeline = build(:ci_empty_pipeline)
+
+ expect(pipeline.merge_request_diff).to be_nil
+ end
+ end
+
+ context 'when the pipeline has a merge request' do
+ context 'when the pipeline is a merged result pipeline' do
+ it 'returns the diff for the source sha' do
+ pipeline = create(:ci_pipeline, :merged_result_pipeline)
+
+ expect(pipeline.merge_request_diff.head_commit_sha).to eq(pipeline.source_sha)
+ end
+ end
+
+ context 'when the pipeline is not a merged result pipeline' do
+ it 'returns the diff for the pipeline sha' do
+ pipeline = create(:ci_pipeline, merge_request: create(:merge_request))
+
+ expect(pipeline.merge_request_diff.head_commit_sha).to eq(pipeline.sha)
+ end
+ end
+ end
+ end
+
describe 'partitioning' do
let(:pipeline) { build(:ci_pipeline, partition_id: nil) }
diff --git a/spec/models/ci/processable_spec.rb b/spec/models/ci/processable_spec.rb
index 07fac4ee2f7..db22d8f3a6c 100644
--- a/spec/models/ci/processable_spec.rb
+++ b/spec/models/ci/processable_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::Processable do
+RSpec.describe Ci::Processable, feature_category: :continuous_integration do
let_it_be(:project) { create(:project) }
let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
@@ -83,7 +83,7 @@ RSpec.describe Ci::Processable do
runner_id tag_taggings taggings tags trigger_request_id
user_id auto_canceled_by_id retried failure_reason
sourced_pipelines sourced_pipeline artifacts_file_store artifacts_metadata_store
- metadata runner_session trace_chunks upstream_pipeline_id
+ metadata runner_machine_id runner_machine runner_session trace_chunks upstream_pipeline_id
artifacts_file artifacts_metadata artifacts_size commands
resource resource_group_id processed security_scans author
pipeline_id report_results pending_state pages_deployments
@@ -287,6 +287,12 @@ RSpec.describe Ci::Processable do
end
end
+ context 'when the processable is a bridge' do
+ subject(:processable) { create(:ci_bridge, pipeline: pipeline) }
+
+ it_behaves_like 'retryable processable'
+ end
+
context 'when the processable is a build' do
subject(:processable) { create(:ci_build, pipeline: pipeline) }
diff --git a/spec/models/ci/runner_machine_spec.rb b/spec/models/ci/runner_machine_spec.rb
index e39f987110f..d0979d8a485 100644
--- a/spec/models/ci/runner_machine_spec.rb
+++ b/spec/models/ci/runner_machine_spec.rb
@@ -6,11 +6,14 @@ RSpec.describe Ci::RunnerMachine, feature_category: :runner_fleet, type: :model
it_behaves_like 'having unique enum values'
it { is_expected.to belong_to(:runner) }
+ it { is_expected.to belong_to(:runner_version).with_foreign_key(:version) }
+ it { is_expected.to have_many(:build_metadata) }
+ it { is_expected.to have_many(:builds).through(:build_metadata) }
describe 'validation' do
it { is_expected.to validate_presence_of(:runner) }
- it { is_expected.to validate_presence_of(:machine_xid) }
- it { is_expected.to validate_length_of(:machine_xid).is_at_most(64) }
+ it { is_expected.to validate_presence_of(:system_xid) }
+ it { is_expected.to validate_length_of(:system_xid).is_at_most(64) }
it { is_expected.to validate_length_of(:version).is_at_most(2048) }
it { is_expected.to validate_length_of(:revision).is_at_most(255) }
it { is_expected.to validate_length_of(:platform).is_at_most(255) }
@@ -37,10 +40,11 @@ RSpec.describe Ci::RunnerMachine, feature_category: :runner_fleet, type: :model
describe '.stale', :freeze_time do
subject { described_class.stale.ids }
- let!(:runner_machine1) { create(:ci_runner_machine, created_at: 8.days.ago, contacted_at: 7.days.ago) }
- let!(:runner_machine2) { create(:ci_runner_machine, created_at: 7.days.ago, contacted_at: nil) }
- let!(:runner_machine3) { create(:ci_runner_machine, created_at: 5.days.ago, contacted_at: nil) }
- let!(:runner_machine4) do
+ let!(:runner_machine1) { create(:ci_runner_machine, :stale) }
+ let!(:runner_machine2) { create(:ci_runner_machine, :stale, contacted_at: nil) }
+ let!(:runner_machine3) { create(:ci_runner_machine, created_at: 6.months.ago, contacted_at: Time.current) }
+ let!(:runner_machine4) { create(:ci_runner_machine, created_at: 5.days.ago) }
+ let!(:runner_machine5) do
create(:ci_runner_machine, created_at: (7.days - 1.second).ago, contacted_at: (7.days - 1.second).ago)
end
@@ -48,4 +52,146 @@ RSpec.describe Ci::RunnerMachine, feature_category: :runner_fleet, type: :model
is_expected.to match_array([runner_machine1.id, runner_machine2.id])
end
end
+
+ describe '#heartbeat', :freeze_time do
+ let(:runner_machine) { create(:ci_runner_machine) }
+ let(:executor) { 'shell' }
+ let(:version) { '15.0.1' }
+ let(:values) do
+ {
+ ip_address: '8.8.8.8',
+ architecture: '18-bit',
+ config: { gpus: "all" },
+ executor: executor,
+ version: version
+ }
+ end
+
+ subject(:heartbeat) do
+ runner_machine.heartbeat(values)
+ end
+
+ context 'when database was updated recently' do
+ before do
+ runner_machine.contacted_at = Time.current
+ end
+
+ it 'schedules version update' do
+ expect(Ci::Runners::ProcessRunnerVersionUpdateWorker).to receive(:perform_async).with(version).once
+
+ heartbeat
+
+ expect(runner_machine.runner_version).to be_nil
+ end
+
+ it 'updates cache' do
+ expect_redis_update
+
+ heartbeat
+ end
+
+ context 'with only ip_address specified' do
+ let(:values) do
+ { ip_address: '1.1.1.1' }
+ end
+
+ it 'updates only ip_address' do
+ attrs = Gitlab::Json.dump(ip_address: '1.1.1.1', contacted_at: Time.current)
+
+ Gitlab::Redis::Cache.with do |redis|
+ redis_key = runner_machine.send(:cache_attribute_key)
+ expect(redis).to receive(:set).with(redis_key, attrs, any_args)
+ end
+
+ heartbeat
+ end
+ end
+ end
+
+ context 'when database was not updated recently' do
+ before do
+ runner_machine.contacted_at = 2.hours.ago
+
+ allow(Ci::Runners::ProcessRunnerVersionUpdateWorker).to receive(:perform_async).with(version).once
+ end
+
+ context 'with invalid runner_machine' do
+ before do
+ runner_machine.runner = nil
+ end
+
+ it 'still updates redis cache and database' do
+ expect(runner_machine).to be_invalid
+
+ expect_redis_update
+ does_db_update
+
+ expect(Ci::Runners::ProcessRunnerVersionUpdateWorker).to have_received(:perform_async)
+ .with(version).once
+ end
+ end
+
+ context 'with unchanged runner_machine version' do
+ let(:runner_machine) { create(:ci_runner_machine, version: version) }
+
+ it 'does not schedule ci_runner_versions update' do
+ heartbeat
+
+ expect(Ci::Runners::ProcessRunnerVersionUpdateWorker).not_to have_received(:perform_async)
+ end
+ end
+
+ it 'updates redis cache and database' do
+ expect_redis_update
+ does_db_update
+
+ expect(Ci::Runners::ProcessRunnerVersionUpdateWorker).to have_received(:perform_async)
+ .with(version).once
+ end
+
+ Ci::Runner::EXECUTOR_NAME_TO_TYPES.each_key do |executor|
+ context "with #{executor} executor" do
+ let(:executor) { executor }
+
+ it 'updates with expected executor type' do
+ expect_redis_update
+
+ heartbeat
+
+ expect(runner_machine.reload.read_attribute(:executor_type)).to eq(expected_executor_type)
+ end
+
+ def expected_executor_type
+ executor.gsub(/[+-]/, '_')
+ end
+ end
+ end
+
+ context "with an unknown executor type" do
+ let(:executor) { 'some-unknown-type' }
+
+ it 'updates with unknown executor type' do
+ expect_redis_update
+
+ heartbeat
+
+ expect(runner_machine.reload.read_attribute(:executor_type)).to eq('unknown')
+ end
+ end
+ end
+
+ def expect_redis_update
+ Gitlab::Redis::Cache.with do |redis|
+ redis_key = runner_machine.send(:cache_attribute_key)
+ expect(redis).to receive(:set).with(redis_key, anything, any_args).and_call_original
+ end
+ end
+
+ def does_db_update
+ expect { heartbeat }.to change { runner_machine.reload.read_attribute(:contacted_at) }
+ .and change { runner_machine.reload.read_attribute(:architecture) }
+ .and change { runner_machine.reload.read_attribute(:config) }
+ .and change { runner_machine.reload.read_attribute(:executor_type) }
+ end
+ end
end
diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb
index b7c7b67b98f..01d5fe7f90b 100644
--- a/spec/models/ci/runner_spec.rb
+++ b/spec/models/ci/runner_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::Runner, feature_category: :runner do
+RSpec.describe Ci::Runner, type: :model, feature_category: :runner do
include StubGitlabCalls
it_behaves_like 'having unique enum values'
@@ -85,6 +85,7 @@ RSpec.describe Ci::Runner, feature_category: :runner do
describe 'validation' do
it { is_expected.to validate_presence_of(:access_level) }
it { is_expected.to validate_presence_of(:runner_type) }
+ it { is_expected.to validate_presence_of(:registration_type) }
context 'when runner is not allowed to pick untagged jobs' do
context 'when runner does not have tags' do
@@ -259,16 +260,16 @@ RSpec.describe Ci::Runner, feature_category: :runner do
end
describe '.belonging_to_project' do
- it 'returns the specific project runner' do
+ it 'returns the project runner' do
# own
- specific_project = create(:project)
- specific_runner = create(:ci_runner, :project, projects: [specific_project])
+ own_project = create(:project)
+ own_runner = create(:ci_runner, :project, projects: [own_project])
# other
other_project = create(:project)
create(:ci_runner, :project, projects: [other_project])
- expect(described_class.belonging_to_project(specific_project.id)).to eq [specific_runner]
+ expect(described_class.belonging_to_project(own_project.id)).to eq [own_runner]
end
end
@@ -285,7 +286,7 @@ RSpec.describe Ci::Runner, feature_category: :runner do
subject(:result) { described_class.belonging_to_parent_group_of_project(project_id) }
- it 'returns the specific group runner' do
+ it 'returns the group runner' do
expect(result).to contain_exactly(runner1)
end
@@ -339,7 +340,7 @@ RSpec.describe Ci::Runner, feature_category: :runner do
describe '.owned_or_instance_wide' do
subject { described_class.owned_or_instance_wide(project.id) }
- it 'returns a globally shared, a project specific and a group specific runner' do
+ it 'returns a shared, project and group runner' do
is_expected.to contain_exactly(group_runner, project_runner, shared_runner)
end
end
@@ -352,7 +353,7 @@ RSpec.describe Ci::Runner, feature_category: :runner do
project_runner
end
- it 'returns a globally shared and a group specific runner' do
+ it 'returns a globally shared and a group runner' do
is_expected.to contain_exactly(group_runner, shared_runner)
end
end
@@ -382,7 +383,7 @@ RSpec.describe Ci::Runner, feature_category: :runner do
context 'with group runners disabled' do
let(:group_runners_enabled) { false }
- it 'returns only the project specific runner' do
+ it 'returns only the project runner' do
is_expected.to contain_exactly(project_runner)
end
end
@@ -390,7 +391,7 @@ RSpec.describe Ci::Runner, feature_category: :runner do
context 'with group runners enabled' do
let(:group_runners_enabled) { true }
- it 'returns a project specific and a group specific runner' do
+ it 'returns a project runner and a group runner' do
is_expected.to contain_exactly(group_runner, project_runner)
end
end
@@ -404,7 +405,7 @@ RSpec.describe Ci::Runner, feature_category: :runner do
project_runner
end
- it 'returns a group specific runner' do
+ it 'returns a group runner' do
is_expected.to contain_exactly(group_runner)
end
end
@@ -1737,6 +1738,40 @@ RSpec.describe Ci::Runner, feature_category: :runner do
end
end
+ describe '#short_sha' do
+ subject(:short_sha) { runner.short_sha }
+
+ context 'when registered via command-line' do
+ let(:runner) { create(:ci_runner) }
+
+ specify { expect(runner.token).not_to start_with(described_class::CREATED_RUNNER_TOKEN_PREFIX) }
+ it { is_expected.not_to start_with(described_class::CREATED_RUNNER_TOKEN_PREFIX) }
+ end
+
+ context 'when creating new runner via UI' do
+ let(:runner) { create(:ci_runner, registration_type: :authenticated_user) }
+
+ specify { expect(runner.token).to start_with(described_class::CREATED_RUNNER_TOKEN_PREFIX) }
+ it { is_expected.not_to start_with(described_class::CREATED_RUNNER_TOKEN_PREFIX) }
+ end
+ end
+
+ describe '#token' do
+ subject(:token) { runner.token }
+
+ context 'when runner is registered' do
+ let(:runner) { create(:ci_runner) }
+
+ it { is_expected.not_to start_with('glrt-') }
+ end
+
+ context 'when runner is created via UI' do
+ let(:runner) { create(:ci_runner, registration_type: :authenticated_user) }
+
+ it { is_expected.to start_with('glrt-') }
+ end
+ end
+
describe '#token_expires_at', :freeze_time do
shared_examples 'expiring token' do |interval:|
it 'expires' do
@@ -1915,7 +1950,7 @@ RSpec.describe Ci::Runner, feature_category: :runner do
end
end
- describe '#with_upgrade_status' do
+ describe '.with_upgrade_status' do
subject { described_class.with_upgrade_status(upgrade_status) }
let_it_be(:runner_14_0_0) { create(:ci_runner, version: '14.0.0') }
@@ -1923,12 +1958,12 @@ RSpec.describe Ci::Runner, feature_category: :runner do
let_it_be(:runner_14_1_1) { create(:ci_runner, version: '14.1.1') }
let_it_be(:runner_version_14_0_0) { create(:ci_runner_version, version: '14.0.0', status: :available) }
let_it_be(:runner_version_14_1_0) { create(:ci_runner_version, version: '14.1.0', status: :recommended) }
- let_it_be(:runner_version_14_1_1) { create(:ci_runner_version, version: '14.1.1', status: :not_available) }
+ let_it_be(:runner_version_14_1_1) { create(:ci_runner_version, version: '14.1.1', status: :unavailable) }
- context ':not_available' do
- let(:upgrade_status) { :not_available }
+ context ':unavailable' do
+ let(:upgrade_status) { :unavailable }
- it 'returns runners whose version is assigned :not_available' do
+ it 'returns runners whose version is assigned :unavailable' do
is_expected.to contain_exactly(runner_14_1_1)
end
end
diff --git a/spec/models/ci/runner_version_spec.rb b/spec/models/ci/runner_version_spec.rb
index dfaa2201859..51a2f14c57c 100644
--- a/spec/models/ci/runner_version_spec.rb
+++ b/spec/models/ci/runner_version_spec.rb
@@ -3,33 +3,35 @@
require 'spec_helper'
RSpec.describe Ci::RunnerVersion, feature_category: :runner_fleet do
- let_it_be(:runner_version_recommended) do
+ let_it_be(:runner_version_upgrade_recommended) do
create(:ci_runner_version, version: 'abc234', status: :recommended)
end
- let_it_be(:runner_version_not_available) do
- create(:ci_runner_version, version: 'abc123', status: :not_available)
+ let_it_be(:runner_version_upgrade_unavailable) do
+ create(:ci_runner_version, version: 'abc123', status: :unavailable)
end
+ it { is_expected.to have_many(:runner_machines).with_foreign_key(:version) }
+
it_behaves_like 'having unique enum values'
- describe '.not_available' do
- subject { described_class.not_available }
+ describe '.unavailable' do
+ subject { described_class.unavailable }
- it { is_expected.to match_array([runner_version_not_available]) }
+ it { is_expected.to match_array([runner_version_upgrade_unavailable]) }
end
describe '.potentially_outdated' do
subject { described_class.potentially_outdated }
let_it_be(:runner_version_nil) { create(:ci_runner_version, version: 'abc345', status: nil) }
- let_it_be(:runner_version_available) do
+ let_it_be(:runner_version_upgrade_available) do
create(:ci_runner_version, version: 'abc456', status: :available)
end
it 'contains any valid or unprocessed runner version that is not already recommended' do
is_expected.to match_array(
- [runner_version_nil, runner_version_not_available, runner_version_available]
+ [runner_version_nil, runner_version_upgrade_unavailable, runner_version_upgrade_available]
)
end
end
diff --git a/spec/models/ci/running_build_spec.rb b/spec/models/ci/running_build_spec.rb
index 1a5ea044ba3..7f254bd235c 100644
--- a/spec/models/ci/running_build_spec.rb
+++ b/spec/models/ci/running_build_spec.rb
@@ -31,7 +31,7 @@ RSpec.describe Ci::RunningBuild, feature_category: :continuous_integration do
end
end
- context 'when build has been picked by a specific runner' do
+ context 'when build has been picked by a project runner' do
let(:runner) { create(:ci_runner, :project) }
it 'raises an error' do
diff --git a/spec/models/ci/secure_file_spec.rb b/spec/models/ci/secure_file_spec.rb
index 87077fe2db1..38ae908fb00 100644
--- a/spec/models/ci/secure_file_spec.rb
+++ b/spec/models/ci/secure_file_spec.rb
@@ -101,6 +101,11 @@ RSpec.describe Ci::SecureFile do
file = build(:ci_secure_file, name: 'file1.tar.gz')
expect(file.file_extension).to eq('gz')
end
+
+ it 'returns nil if there is no file extension' do
+ file = build(:ci_secure_file, name: 'file1')
+ expect(file.file_extension).to be nil
+ end
end
describe '#metadata_parsable?' do
diff --git a/spec/models/ci/trigger_spec.rb b/spec/models/ci/trigger_spec.rb
index 8517e583ec7..5eef719ae0c 100644
--- a/spec/models/ci/trigger_spec.rb
+++ b/spec/models/ci/trigger_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::Trigger do
+RSpec.describe Ci::Trigger, feature_category: :continuous_integration do
let(:project) { create :project }
describe 'associations' do
@@ -86,4 +86,40 @@ RSpec.describe Ci::Trigger do
let!(:model) { create(:ci_trigger, project: parent) }
end
end
+
+ describe 'encrypted_token' do
+ context 'when token is not provided' do
+ it 'encrypts the generated token' do
+ trigger = create(:ci_trigger_without_token, project: project)
+
+ expect(trigger.token).not_to be_nil
+ expect(trigger.encrypted_token).not_to be_nil
+ expect(trigger.encrypted_token_iv).not_to be_nil
+
+ expect(trigger.reload.encrypted_token_tmp).to eq(trigger.token)
+ end
+ end
+
+ context 'when token is provided' do
+ it 'encrypts the given token' do
+ trigger = create(:ci_trigger, project: project)
+
+ expect(trigger.token).not_to be_nil
+ expect(trigger.encrypted_token).not_to be_nil
+ expect(trigger.encrypted_token_iv).not_to be_nil
+
+ expect(trigger.reload.encrypted_token_tmp).to eq(trigger.token)
+ end
+ end
+
+ context 'when token is being updated' do
+ it 'encrypts the given token' do
+ trigger = create(:ci_trigger, project: project, token: "token")
+ expect { trigger.update!(token: "new token") }
+ .to change { trigger.encrypted_token }
+ .and change { trigger.encrypted_token_iv }
+ .and change { trigger.encrypted_token_tmp }.from("token").to("new token")
+ end
+ end
+ end
end
diff --git a/spec/models/ci/variable_spec.rb b/spec/models/ci/variable_spec.rb
index 5f2b5971508..ce64b3ea158 100644
--- a/spec/models/ci/variable_spec.rb
+++ b/spec/models/ci/variable_spec.rb
@@ -2,10 +2,13 @@
require 'spec_helper'
-RSpec.describe Ci::Variable do
- subject { build(:ci_variable) }
+RSpec.describe Ci::Variable, feature_category: :pipeline_authoring do
+ let_it_be_with_reload(:project) { create(:project) }
+
+ subject { build(:ci_variable, project: project) }
it_behaves_like "CI variable"
+ it_behaves_like 'includes Limitable concern'
describe 'validations' do
it { is_expected.to include_module(Presentable) }
diff --git a/spec/models/ci_platform_metric_spec.rb b/spec/models/ci_platform_metric_spec.rb
index f73db713791..e59730792b8 100644
--- a/spec/models/ci_platform_metric_spec.rb
+++ b/spec/models/ci_platform_metric_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe CiPlatformMetric do
+RSpec.describe CiPlatformMetric, feature_category: :continuous_integration do
subject { build(:ci_platform_metric) }
it_behaves_like 'a BulkInsertSafe model', CiPlatformMetric do
diff --git a/spec/models/clusters/applications/cert_manager_spec.rb b/spec/models/clusters/applications/cert_manager_spec.rb
deleted file mode 100644
index 427a99efadd..00000000000
--- a/spec/models/clusters/applications/cert_manager_spec.rb
+++ /dev/null
@@ -1,157 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Clusters::Applications::CertManager do
- let(:cert_manager) { create(:clusters_applications_cert_manager) }
-
- include_examples 'cluster application core specs', :clusters_applications_cert_manager
- include_examples 'cluster application status specs', :clusters_applications_cert_manager
- include_examples 'cluster application version specs', :clusters_applications_cert_manager
- include_examples 'cluster application initial status specs'
-
- describe 'default values' do
- it { expect(cert_manager.version).to eq(described_class::VERSION) }
- it { expect(cert_manager.email).to eq("admin@example.com") }
- end
-
- describe '#can_uninstall?' do
- subject { cert_manager.can_uninstall? }
-
- it { is_expected.to be_truthy }
- end
-
- describe '#install_command' do
- let(:cert_email) { 'admin@example.com' }
-
- let(:cluster_issuer_file) do
- file_contents = <<~EOF
- ---
- apiVersion: certmanager.k8s.io/v1alpha1
- kind: ClusterIssuer
- metadata:
- name: letsencrypt-prod
- spec:
- acme:
- server: https://acme-v02.api.letsencrypt.org/directory
- email: #{cert_email}
- privateKeySecretRef:
- name: letsencrypt-prod
- http01: {}
- EOF
-
- { "cluster_issuer.yaml": file_contents }
- end
-
- subject { cert_manager.install_command }
-
- it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::Helm::V3::InstallCommand) }
-
- it 'is initialized with cert_manager arguments' do
- expect(subject.name).to eq('certmanager')
- expect(subject.chart).to eq('certmanager/cert-manager')
- expect(subject.repository).to eq('https://charts.jetstack.io')
- expect(subject.version).to eq('v0.10.1')
- expect(subject).to be_rbac
- expect(subject.files).to eq(cert_manager.files.merge(cluster_issuer_file))
- expect(subject.preinstall).to eq(
- [
- 'kubectl apply -f https://raw.githubusercontent.com/jetstack/cert-manager/release-0.10/deploy/manifests/00-crds.yaml',
- 'kubectl label --overwrite namespace gitlab-managed-apps certmanager.k8s.io/disable-validation=true'
- ])
- expect(subject.postinstall).to eq(
- [
- "for i in $(seq 1 90); do kubectl apply -f /data/helm/certmanager/config/cluster_issuer.yaml && s=0 && break || s=$?; sleep 1s; echo \"Retrying ($i)...\"; done; (exit $s)"
- ])
- end
-
- context 'for a specific user' do
- let(:cert_email) { 'abc@xyz.com' }
-
- before do
- cert_manager.email = cert_email
- end
-
- it 'uses their email to register issuer with certificate provider' do
- expect(subject.files).to eq(cert_manager.files.merge(cluster_issuer_file))
- end
- end
-
- context 'on a non rbac enabled cluster' do
- before do
- cert_manager.cluster.platform_kubernetes.abac!
- end
-
- it { is_expected.not_to be_rbac }
- end
-
- context 'application failed to install previously' do
- let(:cert_manager) { create(:clusters_applications_cert_manager, :errored, version: '0.0.1') }
-
- it 'is initialized with the locked version' do
- expect(subject.version).to eq('v0.10.1')
- end
- end
- end
-
- describe '#uninstall_command' do
- subject { cert_manager.uninstall_command }
-
- it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::Helm::V3::DeleteCommand) }
-
- it 'is initialized with cert_manager arguments' do
- expect(subject.name).to eq('certmanager')
- expect(subject).to be_rbac
- expect(subject.files).to eq(cert_manager.files)
- end
-
- it 'specifies a post delete command to remove custom resource definitions' do
- expect(subject.postdelete).to eq(
- [
- 'kubectl delete secret -n gitlab-managed-apps letsencrypt-prod --ignore-not-found',
- 'kubectl delete crd certificates.certmanager.k8s.io --ignore-not-found',
- 'kubectl delete crd certificaterequests.certmanager.k8s.io --ignore-not-found',
- 'kubectl delete crd challenges.certmanager.k8s.io --ignore-not-found',
- 'kubectl delete crd clusterissuers.certmanager.k8s.io --ignore-not-found',
- 'kubectl delete crd issuers.certmanager.k8s.io --ignore-not-found',
- 'kubectl delete crd orders.certmanager.k8s.io --ignore-not-found'
- ])
- end
-
- context 'secret key name is not found' do
- before do
- allow(File).to receive(:read).and_call_original
- expect(File).to receive(:read)
- .with(Rails.root.join('vendor', 'cert_manager', 'cluster_issuer.yaml'))
- .and_return('key: value')
- end
-
- it 'does not try and delete the secret' do
- expect(subject.postdelete).to eq(
- [
- 'kubectl delete crd certificates.certmanager.k8s.io --ignore-not-found',
- 'kubectl delete crd certificaterequests.certmanager.k8s.io --ignore-not-found',
- 'kubectl delete crd challenges.certmanager.k8s.io --ignore-not-found',
- 'kubectl delete crd clusterissuers.certmanager.k8s.io --ignore-not-found',
- 'kubectl delete crd issuers.certmanager.k8s.io --ignore-not-found',
- 'kubectl delete crd orders.certmanager.k8s.io --ignore-not-found'
- ])
- end
- end
- end
-
- describe '#files' do
- let(:application) { cert_manager }
- let(:values) { subject[:'values.yaml'] }
-
- subject { application.files }
-
- it 'includes cert_manager specific keys in the values.yaml file' do
- expect(values).to include('ingressShim')
- end
- end
-
- describe 'validations' do
- it { is_expected.to validate_presence_of(:email) }
- end
-end
diff --git a/spec/models/clusters/applications/cilium_spec.rb b/spec/models/clusters/applications/cilium_spec.rb
deleted file mode 100644
index 8b01502d5c0..00000000000
--- a/spec/models/clusters/applications/cilium_spec.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Clusters::Applications::Cilium do
- let(:cilium) { create(:clusters_applications_cilium) }
-
- include_examples 'cluster application core specs', :clusters_applications_cilium
- include_examples 'cluster application status specs', :clusters_applications_cilium
- include_examples 'cluster application initial status specs'
-
- describe '#allowed_to_uninstall?' do
- subject { cilium.allowed_to_uninstall? }
-
- it { is_expected.to be false }
- end
-end
diff --git a/spec/models/clusters/cluster_spec.rb b/spec/models/clusters/cluster_spec.rb
index be64d72e031..2a2e2899d24 100644
--- a/spec/models/clusters/cluster_spec.rb
+++ b/spec/models/clusters/cluster_spec.rb
@@ -2,13 +2,14 @@
require 'spec_helper'
-RSpec.describe Clusters::Cluster, :use_clean_rails_memory_store_caching do
+RSpec.describe Clusters::Cluster, :use_clean_rails_memory_store_caching,
+feature_category: :kubernetes_management do
include ReactiveCachingHelpers
include KubernetesHelpers
it_behaves_like 'having unique enum values'
- subject { build(:cluster) }
+ subject(:cluster) { build(:cluster) }
it { is_expected.to include_module(HasEnvironmentScope) }
it { is_expected.to belong_to(:user) }
@@ -35,14 +36,6 @@ RSpec.describe Clusters::Cluster, :use_clean_rails_memory_store_caching do
it { is_expected.to delegate_method(:status).to(:provider) }
it { is_expected.to delegate_method(:status_reason).to(:provider) }
- it { is_expected.to delegate_method(:on_creation?).to(:provider) }
- it { is_expected.to delegate_method(:knative_pre_installed?).to(:provider) }
- it { is_expected.to delegate_method(:active?).to(:platform_kubernetes).with_prefix }
- it { is_expected.to delegate_method(:rbac?).to(:platform_kubernetes).with_prefix }
- it { is_expected.to delegate_method(:available?).to(:application_helm).with_prefix }
- it { is_expected.to delegate_method(:available?).to(:application_ingress).with_prefix }
- it { is_expected.to delegate_method(:available?).to(:application_knative).with_prefix }
- it { is_expected.to delegate_method(:available?).to(:integration_prometheus).with_prefix }
it { is_expected.to delegate_method(:external_ip).to(:application_ingress).with_prefix }
it { is_expected.to delegate_method(:external_hostname).to(:application_ingress).with_prefix }
@@ -721,14 +714,13 @@ RSpec.describe Clusters::Cluster, :use_clean_rails_memory_store_caching do
context 'when all applications are created' do
let!(:helm) { create(:clusters_applications_helm, cluster: cluster) }
let!(:ingress) { create(:clusters_applications_ingress, cluster: cluster) }
- let!(:cert_manager) { create(:clusters_applications_cert_manager, cluster: cluster) }
let!(:prometheus) { create(:clusters_applications_prometheus, cluster: cluster) }
let!(:runner) { create(:clusters_applications_runner, cluster: cluster) }
let!(:jupyter) { create(:clusters_applications_jupyter, cluster: cluster) }
let!(:knative) { create(:clusters_applications_knative, cluster: cluster) }
it 'returns a list of created applications' do
- is_expected.to contain_exactly(helm, ingress, cert_manager, prometheus, runner, jupyter, knative)
+ is_expected.to contain_exactly(helm, ingress, prometheus, runner, jupyter, knative)
end
end
@@ -1417,4 +1409,218 @@ RSpec.describe Clusters::Cluster, :use_clean_rails_memory_store_caching do
end
end
end
+
+ describe '#on_creation?' do
+ subject(:on_creation?) { cluster.on_creation? }
+
+ before do
+ allow(cluster).to receive(:provider).and_return(provider)
+ end
+
+ context 'without provider' do
+ let(:provider) {}
+
+ it { is_expected.to eq(false) }
+ end
+
+ context 'with provider' do
+ let(:provider) { instance_double(Clusters::Providers::Gcp, on_creation?: on_creation?) }
+
+ before do
+ allow(cluster).to receive(:provider).and_return(provider)
+ end
+
+ context 'with on_creation? set to true' do
+ let(:on_creation?) { true }
+
+ it { is_expected.to eq(true) }
+ end
+
+ context 'with on_creation? set to false' do
+ let(:on_creation?) { false }
+
+ it { is_expected.to eq(false) }
+ end
+ end
+ end
+
+ describe '#knative_pre_installed?' do
+ subject(:knative_pre_installed?) { cluster.knative_pre_installed? }
+
+ before do
+ allow(cluster).to receive(:provider).and_return(provider)
+ end
+
+ context 'without provider' do
+ let(:provider) {}
+
+ it { is_expected.to eq(false) }
+ end
+
+ context 'with provider' do
+ let(:provider) { instance_double(Clusters::Providers::Aws, knative_pre_installed?: knative_pre_installed?) }
+
+ context 'with knative_pre_installed? set to true' do
+ let(:knative_pre_installed?) { true }
+
+ it { is_expected.to eq(true) }
+ end
+
+ context 'with knative_pre_installed? set to false' do
+ let(:knative_pre_installed?) { false }
+
+ it { is_expected.to eq(false) }
+ end
+ end
+ end
+
+ describe '#platform_kubernetes_active?' do
+ subject(:platform_kubernetes_active?) { cluster.platform_kubernetes_active? }
+
+ before do
+ allow(cluster).to receive(:platform_kubernetes).and_return(platform_kubernetes)
+ end
+
+ context 'without platform_kubernetes' do
+ let(:platform_kubernetes) {}
+
+ it { is_expected.to eq(false) }
+ end
+
+ context 'with platform_kubernetes' do
+ let(:platform_kubernetes) { instance_double(Clusters::Platforms::Kubernetes, active?: active?) }
+
+ context 'with active? set to true' do
+ let(:active?) { true }
+
+ it { is_expected.to eq(true) }
+ end
+
+ context 'with active? set to false' do
+ let(:active?) { false }
+
+ it { is_expected.to eq(false) }
+ end
+ end
+ end
+
+ describe '#platform_kubernetes_rbac?' do
+ subject(:platform_kubernetes_rbac?) { cluster.platform_kubernetes_rbac? }
+
+ before do
+ allow(cluster).to receive(:platform_kubernetes).and_return(platform_kubernetes)
+ end
+
+ context 'without platform_kubernetes' do
+ let(:platform_kubernetes) {}
+
+ it { is_expected.to eq(false) }
+ end
+
+ context 'with platform_kubernetes' do
+ let(:platform_kubernetes) { instance_double(Clusters::Platforms::Kubernetes, rbac?: rbac?) }
+
+ context 'with rbac? set to true' do
+ let(:rbac?) { true }
+
+ it { is_expected.to eq(true) }
+ end
+
+ context 'with rbac? set to false' do
+ let(:rbac?) { false }
+
+ it { is_expected.to eq(false) }
+ end
+ end
+ end
+
+ describe '#application_helm_available?' do
+ subject(:application_helm_available?) { cluster.application_helm_available? }
+
+ before do
+ allow(cluster).to receive(:application_helm).and_return(application_helm)
+ end
+
+ context 'without application_helm' do
+ let(:application_helm) {}
+
+ it { is_expected.to eq(false) }
+ end
+
+ context 'with application_helm' do
+ let(:application_helm) { instance_double(Clusters::Applications::Helm, available?: available?) }
+
+ context 'with available? set to true' do
+ let(:available?) { true }
+
+ it { is_expected.to eq(true) }
+ end
+
+ context 'with available? set to false' do
+ let(:available?) { false }
+
+ it { is_expected.to eq(false) }
+ end
+ end
+ end
+
+ describe '#application_ingress_available?' do
+ subject(:application_ingress_available?) { cluster.application_ingress_available? }
+
+ before do
+ allow(cluster).to receive(:application_ingress).and_return(application_ingress)
+ end
+
+ context 'without application_ingress' do
+ let(:application_ingress) {}
+
+ it { is_expected.to eq(false) }
+ end
+
+ context 'with application_ingress' do
+ let(:application_ingress) { instance_double(Clusters::Applications::Ingress, available?: available?) }
+
+ context 'with available? set to true' do
+ let(:available?) { true }
+
+ it { is_expected.to eq(true) }
+ end
+
+ context 'with available? set to false' do
+ let(:available?) { false }
+
+ it { is_expected.to eq(false) }
+ end
+ end
+ end
+
+ describe '#application_knative_available?' do
+ subject(:application_knative_available?) { cluster.application_knative_available? }
+
+ before do
+ allow(cluster).to receive(:application_knative).and_return(application_knative)
+ end
+
+ context 'without application_knative' do
+ let(:application_knative) {}
+
+ it { is_expected.to eq(false) }
+ end
+
+ context 'with application_knative' do
+ let(:application_knative) { instance_double(Clusters::Applications::Knative, available?: available?) }
+
+ context 'with available? set to true' do
+ let(:available?) { true }
+
+ it { is_expected.to eq(true) }
+ end
+
+ context 'with available? set to false' do
+ let(:available?) { false }
+
+ it { is_expected.to eq(false) }
+ end
+ end
+ end
end
diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb
index 704203ed29c..4ff451af9de 100644
--- a/spec/models/commit_status_spec.rb
+++ b/spec/models/commit_status_spec.rb
@@ -802,64 +802,70 @@ RSpec.describe CommitStatus do
end
describe 'ensure stage assignment' do
- context 'when commit status has a stage_id assigned' do
- let!(:stage) do
- create(:ci_stage, project: project, pipeline: pipeline)
- end
+ before do
+ stub_feature_flags(ci_remove_ensure_stage_service: false)
+ end
- let(:commit_status) do
- create(:commit_status, stage_id: stage.id, name: 'rspec', stage: 'test')
- end
+ context 'when the feature flag ci_remove_ensure_stage_service is disabled' do
+ context 'when commit status has a stage_id assigned' do
+ let!(:stage) do
+ create(:ci_stage, project: project, pipeline: pipeline)
+ end
- it 'does not create a new stage' do
- expect { commit_status }.not_to change { Ci::Stage.count }
- expect(commit_status.stage_id).to eq stage.id
- end
- end
+ let(:commit_status) do
+ create(:commit_status, stage_id: stage.id, name: 'rspec', stage: 'test')
+ end
- context 'when commit status does not have a stage_id assigned' do
- let(:commit_status) do
- create(:commit_status, name: 'rspec', stage: 'test', status: :success)
+ it 'does not create a new stage' do
+ expect { commit_status }.not_to change { Ci::Stage.count }
+ expect(commit_status.stage_id).to eq stage.id
+ end
end
- let(:stage) { Ci::Stage.first }
+ context 'when commit status does not have a stage_id assigned' do
+ let(:commit_status) do
+ create(:commit_status, name: 'rspec', stage: 'test', status: :success)
+ end
- it 'creates a new stage', :sidekiq_might_not_need_inline do
- expect { commit_status }.to change { Ci::Stage.count }.by(1)
+ let(:stage) { Ci::Stage.first }
- expect(stage.name).to eq 'test'
- expect(stage.project).to eq commit_status.project
- expect(stage.pipeline).to eq commit_status.pipeline
- expect(stage.status).to eq commit_status.status
- expect(commit_status.stage_id).to eq stage.id
- end
- end
+ it 'creates a new stage', :sidekiq_might_not_need_inline do
+ expect { commit_status }.to change { Ci::Stage.count }.by(1)
- context 'when commit status does not have stage but it exists' do
- let!(:stage) do
- create(:ci_stage, project: project, pipeline: pipeline, name: 'test')
+ expect(stage.name).to eq 'test'
+ expect(stage.project).to eq commit_status.project
+ expect(stage.pipeline).to eq commit_status.pipeline
+ expect(stage.status).to eq commit_status.status
+ expect(commit_status.stage_id).to eq stage.id
+ end
end
- let(:commit_status) do
- create(:commit_status, project: project, pipeline: pipeline, name: 'rspec', stage: 'test', status: :success)
- end
+ context 'when commit status does not have stage but it exists' do
+ let!(:stage) do
+ create(:ci_stage, project: project, pipeline: pipeline, name: 'test')
+ end
- it 'uses existing stage', :sidekiq_might_not_need_inline do
- expect { commit_status }.not_to change { Ci::Stage.count }
+ let(:commit_status) do
+ create(:commit_status, project: project, pipeline: pipeline, name: 'rspec', stage: 'test', status: :success)
+ end
- expect(commit_status.stage_id).to eq stage.id
- expect(stage.reload.status).to eq commit_status.status
- end
- end
+ it 'uses existing stage', :sidekiq_might_not_need_inline do
+ expect { commit_status }.not_to change { Ci::Stage.count }
- context 'when commit status is being imported' do
- let(:commit_status) do
- create(:commit_status, name: 'rspec', stage: 'test', importing: true)
+ expect(commit_status.stage_id).to eq stage.id
+ expect(stage.reload.status).to eq commit_status.status
+ end
end
- it 'does not create a new stage' do
- expect { commit_status }.not_to change { Ci::Stage.count }
- expect(commit_status.stage_id).not_to be_present
+ context 'when commit status is being imported' do
+ let(:commit_status) do
+ create(:commit_status, name: 'rspec', stage: 'test', importing: true)
+ end
+
+ it 'does not create a new stage' do
+ expect { commit_status }.not_to change { Ci::Stage.count }
+ expect(commit_status.stage_id).not_to be_present
+ end
end
end
end
@@ -1007,6 +1013,10 @@ RSpec.describe CommitStatus do
describe '.stage_name' do
subject(:stage_name) { commit_status.stage_name }
+ before do
+ commit_status.ci_stage = build(:ci_stage)
+ end
+
it 'returns the stage name' do
expect(stage_name).to eq('test')
end
@@ -1023,7 +1033,7 @@ RSpec.describe CommitStatus do
describe 'partitioning' do
context 'with pipeline' do
let(:pipeline) { build(:ci_pipeline, partition_id: 123) }
- let(:status) { build(:commit_status, pipeline: pipeline) }
+ let(:status) { build(:commit_status, pipeline: pipeline, partition_id: nil) }
it 'copies the partition_id from pipeline' do
expect { status.valid? }.to change(status, :partition_id).to(123)
diff --git a/spec/models/concerns/after_commit_queue_spec.rb b/spec/models/concerns/after_commit_queue_spec.rb
index 8f091081dce..c57d388fe5d 100644
--- a/spec/models/concerns/after_commit_queue_spec.rb
+++ b/spec/models/concerns/after_commit_queue_spec.rb
@@ -72,7 +72,7 @@ RSpec.describe AfterCommitQueue do
context 'multiple databases - Ci::ApplicationRecord models' do
before do
- skip_if_multiple_databases_not_setup
+ skip_if_multiple_databases_not_setup(:ci)
table_sql = <<~SQL
CREATE TABLE _test_gitlab_ci_after_commit_queue (
diff --git a/spec/models/concerns/bulk_insert_safe_spec.rb b/spec/models/concerns/bulk_insert_safe_spec.rb
index 577004c2cf6..65b7da20bbc 100644
--- a/spec/models/concerns/bulk_insert_safe_spec.rb
+++ b/spec/models/concerns/bulk_insert_safe_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe BulkInsertSafe do
+RSpec.describe BulkInsertSafe, feature_category: :database do
before(:all) do
ActiveRecord::Schema.define do
create_table :_test_bulk_insert_parent_items, force: true do |t|
diff --git a/spec/models/concerns/ci/has_status_spec.rb b/spec/models/concerns/ci/has_status_spec.rb
index 9dfc7d84f89..4ef690ca4c1 100644
--- a/spec/models/concerns/ci/has_status_spec.rb
+++ b/spec/models/concerns/ci/has_status_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::HasStatus do
+RSpec.describe Ci::HasStatus, feature_category: :continuous_integration do
describe '.composite_status' do
using RSpec::Parameterized::TableSyntax
diff --git a/spec/models/concerns/ci/has_variable_spec.rb b/spec/models/concerns/ci/has_variable_spec.rb
index 861d8f3b974..d7d0cabd4ae 100644
--- a/spec/models/concerns/ci/has_variable_spec.rb
+++ b/spec/models/concerns/ci/has_variable_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Ci::HasVariable do
+RSpec.describe Ci::HasVariable, feature_category: :continuous_integration do
subject { build(:ci_variable) }
it { is_expected.to validate_presence_of(:key) }
@@ -113,4 +113,36 @@ RSpec.describe Ci::HasVariable do
end
end
end
+
+ describe '.order_by' do
+ let_it_be(:relation) { Ci::Variable.all }
+
+ it 'supports ordering by key ascending' do
+ expect(relation).to receive(:reorder).with({ key: :asc })
+
+ relation.order_by('key_asc')
+ end
+
+ it 'supports ordering by key descending' do
+ expect(relation).to receive(:reorder).with({ key: :desc })
+
+ relation.order_by('key_desc')
+ end
+
+ context 'when order method is unknown' do
+ it 'does not call reorder' do
+ expect(relation).not_to receive(:reorder)
+
+ relation.order_by('unknown')
+ end
+ end
+
+ context 'when order method is nil' do
+ it 'does not call reorder' do
+ expect(relation).not_to receive(:reorder)
+
+ relation.order_by(nil)
+ end
+ end
+ end
end
diff --git a/spec/models/concerns/ci/maskable_spec.rb b/spec/models/concerns/ci/maskable_spec.rb
index 2b13fc21fe8..b57b2b15608 100644
--- a/spec/models/concerns/ci/maskable_spec.rb
+++ b/spec/models/concerns/ci/maskable_spec.rb
@@ -2,15 +2,16 @@
require 'spec_helper'
-RSpec.describe Ci::Maskable do
+RSpec.describe Ci::Maskable, feature_category: :pipeline_authoring do
let(:variable) { build(:ci_variable) }
describe 'masked value validations' do
subject { variable }
- context 'when variable is masked' do
+ context 'when variable is masked and expanded' do
before do
subject.masked = true
+ subject.raw = false
end
it { is_expected.not_to allow_value('hello').for(:value) }
@@ -20,6 +21,37 @@ RSpec.describe Ci::Maskable do
it { is_expected.to allow_value('helloworld').for(:value) }
end
+ context 'when method :raw is not defined' do
+ let(:test_var_class) do
+ Struct.new(:masked?) do
+ include ActiveModel::Validations
+ include Ci::Maskable
+ end
+ end
+
+ let(:variable) { test_var_class.new(true) }
+
+ it 'evaluates masked variables as expanded' do
+ expect(subject).not_to be_masked_and_raw
+ expect(subject).to be_masked_and_expanded
+ end
+ end
+
+ context 'when variable is masked and raw' do
+ before do
+ subject.masked = true
+ subject.raw = true
+ end
+
+ it { is_expected.not_to allow_value('hello').for(:value) }
+ it { is_expected.not_to allow_value('hello world').for(:value) }
+ it { is_expected.to allow_value('hello\rworld').for(:value) }
+ it { is_expected.to allow_value('hello$VARIABLEworld').for(:value) }
+ it { is_expected.to allow_value('helloworld!!!').for(:value) }
+ it { is_expected.to allow_value('hell******world').for(:value) }
+ it { is_expected.to allow_value('helloworld123').for(:value) }
+ end
+
context 'when variable is not masked' do
before do
subject.masked = false
@@ -33,40 +65,70 @@ RSpec.describe Ci::Maskable do
end
end
- describe 'REGEX' do
- subject { Ci::Maskable::REGEX }
+ describe 'Regexes' do
+ context 'with MASK_AND_RAW_REGEX' do
+ subject { Ci::Maskable::MASK_AND_RAW_REGEX }
- it 'does not match strings shorter than 8 letters' do
- expect(subject.match?('hello')).to eq(false)
- end
+ it 'does not match strings shorter than 8 letters' do
+ expect(subject.match?('hello')).to eq(false)
+ end
- it 'does not match strings with spaces' do
- expect(subject.match?('hello world')).to eq(false)
- end
+ it 'does not match strings with spaces' do
+ expect(subject.match?('hello world')).to eq(false)
+ end
- it 'does not match strings with shell variables' do
- expect(subject.match?('hello$VARIABLEworld')).to eq(false)
- end
+ it 'does not match strings that span more than one line' do
+ string = <<~EOS
+ hello
+ world
+ EOS
- it 'does not match strings with escape characters' do
- expect(subject.match?('hello\rworld')).to eq(false)
+ expect(subject.match?(string)).to eq(false)
+ end
+
+ it 'matches valid strings' do
+ expect(subject.match?('hello$VARIABLEworld')).to eq(true)
+ expect(subject.match?('Hello+World_123/@:-~.')).to eq(true)
+ expect(subject.match?('hello\rworld')).to eq(true)
+ expect(subject.match?('HelloWorld%#^')).to eq(true)
+ end
end
- it 'does not match strings that span more than one line' do
- string = <<~EOS
- hello
- world
- EOS
+ context 'with REGEX' do
+ subject { Ci::Maskable::REGEX }
- expect(subject.match?(string)).to eq(false)
- end
+ it 'does not match strings shorter than 8 letters' do
+ expect(subject.match?('hello')).to eq(false)
+ end
- it 'does not match strings using unsupported characters' do
- expect(subject.match?('HelloWorld%#^')).to eq(false)
- end
+ it 'does not match strings with spaces' do
+ expect(subject.match?('hello world')).to eq(false)
+ end
- it 'matches valid strings' do
- expect(subject.match?('Hello+World_123/@:-~.')).to eq(true)
+ it 'does not match strings with shell variables' do
+ expect(subject.match?('hello$VARIABLEworld')).to eq(false)
+ end
+
+ it 'does not match strings with escape characters' do
+ expect(subject.match?('hello\rworld')).to eq(false)
+ end
+
+ it 'does not match strings that span more than one line' do
+ string = <<~EOS
+ hello
+ world
+ EOS
+
+ expect(subject.match?(string)).to eq(false)
+ end
+
+ it 'does not match strings using unsupported characters' do
+ expect(subject.match?('HelloWorld%#^')).to eq(false)
+ end
+
+ it 'matches valid strings' do
+ expect(subject.match?('Hello+World_123/@:-~.')).to eq(true)
+ end
end
end
diff --git a/spec/models/concerns/cross_database_modification_spec.rb b/spec/models/concerns/cross_database_modification_spec.rb
index c3831b654cf..eaebf613cb5 100644
--- a/spec/models/concerns/cross_database_modification_spec.rb
+++ b/spec/models/concerns/cross_database_modification_spec.rb
@@ -21,6 +21,14 @@ RSpec.describe CrossDatabaseModification do
expect(ApplicationRecord.gitlab_transactions_stack).to be_empty
+ PackageMetadata::ApplicationRecord.transaction do
+ expect(ApplicationRecord.gitlab_transactions_stack).to contain_exactly(:gitlab_pm)
+
+ Project.first
+ end
+
+ expect(ApplicationRecord.gitlab_transactions_stack).to be_empty
+
Project.transaction do
expect(ApplicationRecord.gitlab_transactions_stack).to contain_exactly(:gitlab_main)
diff --git a/spec/models/concerns/exportable_spec.rb b/spec/models/concerns/exportable_spec.rb
new file mode 100644
index 00000000000..74709b06403
--- /dev/null
+++ b/spec/models/concerns/exportable_spec.rb
@@ -0,0 +1,236 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Exportable, feature_category: :importers do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:milestone) { create(:milestone, project: project) }
+ let_it_be(:issue) { create(:issue, project: project, milestone: milestone) }
+ let_it_be(:note1) { create(:system_note, project: project, noteable: issue) }
+ let_it_be(:note2) { create(:system_note, project: project, noteable: issue) }
+
+ let_it_be(:model_klass) do
+ Class.new(ApplicationRecord) do
+ include Exportable
+
+ belongs_to :project
+ has_one :milestone
+ has_many :notes
+
+ self.table_name = 'issues'
+
+ def self.name
+ 'Issue'
+ end
+ end
+ end
+
+ subject { model_klass.new }
+
+ describe '.readable_records' do
+ let_it_be(:model_record) { model_klass.new }
+
+ context 'when model does not respond to association name' do
+ it 'returns nil' do
+ expect(subject.readable_records(:foo, current_user: user)).to be_nil
+ end
+ end
+
+ context 'when model does respond to association name' do
+ context 'when there are no records' do
+ it 'returns nil' do
+ expect(model_record.readable_records(:notes, current_user: user)).to be_nil
+ end
+ end
+
+ context 'when association has #exportable_record? defined' do
+ before do
+ allow(model_record).to receive(:try).with(:notes).and_return(issue.notes)
+ end
+
+ context 'when user can read all records' do
+ before do
+ allow_next_found_instance_of(Note) do |note|
+ allow(note).to receive(:respond_to?).with(:exportable_record?).and_return(true)
+ allow(note).to receive(:exportable_record?).with(user).and_return(true)
+ end
+ end
+
+ it 'returns collection of readable records' do
+ expect(model_record.readable_records(:notes, current_user: user)).to contain_exactly(note1, note2)
+ end
+ end
+
+ context 'when user can not read records' do
+ before do
+ allow_next_instance_of(Note) do |note|
+ allow(note).to receive(:respond_to?).with(:exportable_record?).and_return(true)
+ allow(note).to receive(:exportable_record?).with(user).and_return(false)
+ end
+ end
+
+ it 'returns collection of readable records' do
+ expect(model_record.readable_records(:notes, current_user: user)).to eq([])
+ end
+ end
+ end
+
+ context 'when association does not have #exportable_record? defined' do
+ before do
+ allow(model_record).to receive(:try).with(:notes).and_return([note1])
+
+ allow(note1).to receive(:respond_to?).and_call_original
+ allow(note1).to receive(:respond_to?).with(:exportable_record?).and_return(false)
+ end
+
+ it 'calls #readable_by?' do
+ expect(note1).to receive(:readable_by?).with(user)
+
+ model_record.readable_records(:notes, current_user: user)
+ end
+ end
+
+ context 'with single relation' do
+ before do
+ allow(model_record).to receive(:try).with(:milestone).and_return(issue.milestone)
+ end
+
+ context 'when user can read the record' do
+ before do
+ allow(milestone).to receive(:readable_by?).with(user).and_return(true)
+ end
+
+ it 'returns collection of readable records' do
+ expect(model_record.readable_records(:milestone, current_user: user)).to eq(milestone)
+ end
+ end
+
+ context 'when user can not read the record' do
+ before do
+ allow(milestone).to receive(:readable_by?).with(user).and_return(false)
+ end
+
+ it 'returns collection of readable records' do
+ expect(model_record.readable_records(:milestone, current_user: user)).to be_nil
+ end
+ end
+ end
+ end
+ end
+
+ describe '.exportable_association?' do
+ context 'when model does not respond to association name' do
+ it 'returns false' do
+ expect(subject.exportable_association?(:tests)).to eq(false)
+
+ allow(issue).to receive(:respond_to?).with(:tests).and_return(false)
+ end
+ end
+
+ context 'when model responds to association name' do
+ let_it_be(:model_record) { model_klass.new }
+
+ context 'when association contains records' do
+ before do
+ allow(model_record).to receive(:try).with(:milestone).and_return(milestone)
+ end
+
+ context 'when current_user is not present' do
+ it 'returns false' do
+ expect(model_record.exportable_association?(:milestone)).to eq(false)
+ end
+ end
+
+ context 'when current_user can read association' do
+ before do
+ allow(milestone).to receive(:readable_by?).with(user).and_return(true)
+ end
+
+ it 'returns true' do
+ expect(model_record.exportable_association?(:milestone, current_user: user)).to eq(true)
+ end
+ end
+
+ context 'when current_user can not read association' do
+ before do
+ allow(milestone).to receive(:readable_by?).with(user).and_return(false)
+ end
+
+ it 'returns false' do
+ expect(model_record.exportable_association?(:milestone, current_user: user)).to eq(false)
+ end
+ end
+ end
+
+ context 'when association is empty' do
+ before do
+ allow(model_record).to receive(:try).with(:milestone).and_return(nil)
+ allow(milestone).to receive(:readable_by?).with(user).and_return(true)
+ end
+
+ it 'returns true' do
+ expect(model_record.exportable_association?(:milestone, current_user: user)).to eq(true)
+ end
+ end
+
+ context 'when association type is has_many' do
+ it 'returns true' do
+ expect(subject.exportable_association?(:notes)).to eq(true)
+ end
+ end
+ end
+ end
+
+ describe '.restricted_associations' do
+ let(:model_associations) { [:notes, :labels] }
+
+ context 'when `exportable_restricted_associations` is not defined in inheriting class' do
+ it 'returns empty array' do
+ expect(subject.restricted_associations(model_associations)).to eq([])
+ end
+ end
+
+ context 'when `exportable_restricted_associations` is defined in inheriting class' do
+ before do
+ stub_const('DummyModel', model_klass)
+
+ DummyModel.class_eval do
+ def exportable_restricted_associations
+ super + [:notes]
+ end
+ end
+ end
+
+ it 'returns empty array if provided key are not restricted' do
+ expect(subject.restricted_associations([:labels])).to eq([])
+ end
+
+ it 'returns array with restricted keys' do
+ expect(subject.restricted_associations(model_associations)).to contain_exactly(:notes)
+ end
+ end
+ end
+
+ describe '.has_many_association?' do
+ let(:model_associations) { [:notes, :labels] }
+
+ context 'when association type is `has_many`' do
+ it 'returns true' do
+ expect(subject.has_many_association?(:notes)).to eq(true)
+ end
+ end
+
+ context 'when association type is `has_one`' do
+ it 'returns true' do
+ expect(subject.has_many_association?(:milestone)).to eq(false)
+ end
+ end
+
+ context 'when association type is `belongs_to`' do
+ it 'returns true' do
+ expect(subject.has_many_association?(:project)).to eq(false)
+ end
+ end
+ end
+end
diff --git a/spec/models/concerns/issuable_link_spec.rb b/spec/models/concerns/issuable_link_spec.rb
index 7be6d8a074d..aaa4de1f46b 100644
--- a/spec/models/concerns/issuable_link_spec.rb
+++ b/spec/models/concerns/issuable_link_spec.rb
@@ -40,4 +40,18 @@ RSpec.describe IssuableLink do
end
end
end
+
+ describe '.available_link_types' do
+ let(:expected_link_types) do
+ if Gitlab.ee?
+ %w[relates_to blocks is_blocked_by]
+ else
+ %w[relates_to]
+ end
+ end
+
+ subject { test_class.available_link_types }
+
+ it { is_expected.to match_array(expected_link_types) }
+ end
end
diff --git a/spec/models/concerns/noteable_spec.rb b/spec/models/concerns/noteable_spec.rb
index 383ed68816e..c1323d20d83 100644
--- a/spec/models/concerns/noteable_spec.rb
+++ b/spec/models/concerns/noteable_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Noteable do
+RSpec.describe Noteable, feature_category: :code_review_workflow do
let!(:active_diff_note1) { create(:diff_note_on_merge_request) }
let(:project) { active_diff_note1.project }
subject { active_diff_note1.noteable }
@@ -155,31 +155,38 @@ RSpec.describe Noteable do
end
describe '#discussion_root_note_ids' do
- let!(:label_event) { create(:resource_label_event, merge_request: subject) }
+ let!(:label_event) do
+ create(:resource_label_event, merge_request: subject).tap do |event|
+ # Create an extra label event that should get grouped with the above event so this one should not
+ # be included in the resulting root nodes
+ create(:resource_label_event, merge_request: subject, user: event.user, created_at: event.created_at)
+ end
+ end
+
let!(:system_note) { create(:system_note, project: project, noteable: subject) }
let!(:milestone_event) { create(:resource_milestone_event, merge_request: subject) }
let!(:state_event) { create(:resource_state_event, merge_request: subject) }
it 'returns ordered discussion_ids and synthetic note ids' do
discussions = subject.discussion_root_note_ids(notes_filter: UserPreference::NOTES_FILTERS[:all_notes]).map do |n|
- { table_name: n.table_name, discussion_id: n.discussion_id, id: n.id }
+ { table_name: n.table_name, id: n.id }
end
expect(discussions).to match(
[
- a_hash_including(table_name: 'notes', discussion_id: active_diff_note1.discussion_id),
- a_hash_including(table_name: 'notes', discussion_id: active_diff_note3.discussion_id),
- a_hash_including(table_name: 'notes', discussion_id: outdated_diff_note1.discussion_id),
- a_hash_including(table_name: 'notes', discussion_id: discussion_note1.discussion_id),
- a_hash_including(table_name: 'notes', discussion_id: commit_diff_note1.discussion_id),
- a_hash_including(table_name: 'notes', discussion_id: commit_note1.discussion_id),
- a_hash_including(table_name: 'notes', discussion_id: commit_note2.discussion_id),
- a_hash_including(table_name: 'notes', discussion_id: commit_discussion_note1.discussion_id),
- a_hash_including(table_name: 'notes', discussion_id: commit_discussion_note3.discussion_id),
- a_hash_including(table_name: 'notes', discussion_id: note1.discussion_id),
- a_hash_including(table_name: 'notes', discussion_id: note2.discussion_id),
+ a_hash_including(table_name: 'notes', id: active_diff_note1.id),
+ a_hash_including(table_name: 'notes', id: active_diff_note3.id),
+ a_hash_including(table_name: 'notes', id: outdated_diff_note1.id),
+ a_hash_including(table_name: 'notes', id: discussion_note1.id),
+ a_hash_including(table_name: 'notes', id: commit_diff_note1.id),
+ a_hash_including(table_name: 'notes', id: commit_note1.id),
+ a_hash_including(table_name: 'notes', id: commit_note2.id),
+ a_hash_including(table_name: 'notes', id: commit_discussion_note1.id),
+ a_hash_including(table_name: 'notes', id: commit_discussion_note3.id),
+ a_hash_including(table_name: 'notes', id: note1.id),
+ a_hash_including(table_name: 'notes', id: note2.id),
a_hash_including(table_name: 'resource_label_events', id: label_event.id),
- a_hash_including(table_name: 'notes', discussion_id: system_note.discussion_id),
+ a_hash_including(table_name: 'notes', id: system_note.id),
a_hash_including(table_name: 'resource_milestone_events', id: milestone_event.id),
a_hash_including(table_name: 'resource_state_events', id: state_event.id)
])
@@ -187,34 +194,34 @@ RSpec.describe Noteable do
it 'filters by comments only' do
discussions = subject.discussion_root_note_ids(notes_filter: UserPreference::NOTES_FILTERS[:only_comments]).map do |n|
- { table_name: n.table_name, discussion_id: n.discussion_id, id: n.id }
+ { table_name: n.table_name, id: n.id }
end
expect(discussions).to match(
[
- a_hash_including(table_name: 'notes', discussion_id: active_diff_note1.discussion_id),
- a_hash_including(table_name: 'notes', discussion_id: active_diff_note3.discussion_id),
- a_hash_including(table_name: 'notes', discussion_id: outdated_diff_note1.discussion_id),
- a_hash_including(table_name: 'notes', discussion_id: discussion_note1.discussion_id),
- a_hash_including(table_name: 'notes', discussion_id: commit_diff_note1.discussion_id),
- a_hash_including(table_name: 'notes', discussion_id: commit_note1.discussion_id),
- a_hash_including(table_name: 'notes', discussion_id: commit_note2.discussion_id),
- a_hash_including(table_name: 'notes', discussion_id: commit_discussion_note1.discussion_id),
- a_hash_including(table_name: 'notes', discussion_id: commit_discussion_note3.discussion_id),
- a_hash_including(table_name: 'notes', discussion_id: note1.discussion_id),
- a_hash_including(table_name: 'notes', discussion_id: note2.discussion_id)
+ a_hash_including(table_name: 'notes', id: active_diff_note1.id),
+ a_hash_including(table_name: 'notes', id: active_diff_note3.id),
+ a_hash_including(table_name: 'notes', id: outdated_diff_note1.id),
+ a_hash_including(table_name: 'notes', id: discussion_note1.id),
+ a_hash_including(table_name: 'notes', id: commit_diff_note1.id),
+ a_hash_including(table_name: 'notes', id: commit_note1.id),
+ a_hash_including(table_name: 'notes', id: commit_note2.id),
+ a_hash_including(table_name: 'notes', id: commit_discussion_note1.id),
+ a_hash_including(table_name: 'notes', id: commit_discussion_note3.id),
+ a_hash_including(table_name: 'notes', id: note1.id),
+ a_hash_including(table_name: 'notes', id: note2.id)
])
end
it 'filters by system notes only' do
discussions = subject.discussion_root_note_ids(notes_filter: UserPreference::NOTES_FILTERS[:only_activity]).map do |n|
- { table_name: n.table_name, discussion_id: n.discussion_id, id: n.id }
+ { table_name: n.table_name, id: n.id }
end
expect(discussions).to match(
[
a_hash_including(table_name: 'resource_label_events', id: label_event.id),
- a_hash_including(table_name: 'notes', discussion_id: system_note.discussion_id),
+ a_hash_including(table_name: 'notes', id: system_note.id),
a_hash_including(table_name: 'resource_milestone_events', id: milestone_event.id),
a_hash_including(table_name: 'resource_state_events', id: state_event.id)
])
diff --git a/spec/models/concerns/pg_full_text_searchable_spec.rb b/spec/models/concerns/pg_full_text_searchable_spec.rb
index 87f1dc5a27b..059df64f7d0 100644
--- a/spec/models/concerns/pg_full_text_searchable_spec.rb
+++ b/spec/models/concerns/pg_full_text_searchable_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe PgFullTextSearchable do
+RSpec.describe PgFullTextSearchable, feature_category: :global_search do
let(:project) { build(:project, project_namespace: build(:project_namespace)) }
let(:model_class) do
diff --git a/spec/models/concerns/require_email_verification_spec.rb b/spec/models/concerns/require_email_verification_spec.rb
index d087b2864f8..0a6293f852e 100644
--- a/spec/models/concerns/require_email_verification_spec.rb
+++ b/spec/models/concerns/require_email_verification_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe RequireEmailVerification do
+RSpec.describe RequireEmailVerification, feature_category: :insider_threat do
let_it_be(:model) do
Class.new(ApplicationRecord) do
self.table_name = 'users'
@@ -15,11 +15,15 @@ RSpec.describe RequireEmailVerification do
using RSpec::Parameterized::TableSyntax
- where(:feature_flag_enabled, :two_factor_enabled, :overridden) do
- false | false | false
- false | true | false
- true | false | true
- true | true | false
+ where(:feature_flag_enabled, :two_factor_enabled, :skipped, :overridden) do
+ false | false | false | false
+ false | false | true | false
+ false | true | false | false
+ false | true | true | false
+ true | false | false | true
+ true | false | true | false
+ true | true | false | false
+ true | true | true | false
end
with_them do
@@ -29,6 +33,7 @@ RSpec.describe RequireEmailVerification do
before do
stub_feature_flags(require_email_verification: feature_flag_enabled ? instance : another_instance)
allow(instance).to receive(:two_factor_enabled?).and_return(two_factor_enabled)
+ stub_feature_flags(skip_require_email_verification: skipped ? instance : another_instance)
end
describe '#lock_access!' do
diff --git a/spec/models/concerns/sensitive_serializable_hash_spec.rb b/spec/models/concerns/sensitive_serializable_hash_spec.rb
index 0bfd2d6a7de..7d646106061 100644
--- a/spec/models/concerns/sensitive_serializable_hash_spec.rb
+++ b/spec/models/concerns/sensitive_serializable_hash_spec.rb
@@ -46,8 +46,8 @@ RSpec.describe SensitiveSerializableHash do
context "#{klass.name}\##{attribute_name}" do
let(:attributes) { [attribute_name, "encrypted_#{attribute_name}", "encrypted_#{attribute_name}_iv"] }
- it 'has a encrypted_attributes field' do
- expect(klass.encrypted_attributes).to include(attribute_name.to_sym)
+ it 'has a attr_encrypted_attributes field' do
+ expect(klass.attr_encrypted_attributes).to include(attribute_name.to_sym)
end
it 'does not include the attribute in serializable_hash', :aggregate_failures do
diff --git a/spec/models/concerns/spammable_spec.rb b/spec/models/concerns/spammable_spec.rb
index baa2d75705a..44cf87aa1c1 100644
--- a/spec/models/concerns/spammable_spec.rb
+++ b/spec/models/concerns/spammable_spec.rb
@@ -202,5 +202,21 @@ RSpec.describe Spammable do
expect(issue.submittable_as_spam_by?(nil)).to be_nil
end
end
+
+ describe '#allow_possible_spam?' do
+ subject { issue.allow_possible_spam? }
+
+ context 'when the `allow_possible_spam` application setting is turned off' do
+ it { is_expected.to eq(false) }
+ end
+
+ context 'when the `allow_possible_spam` application setting is turned on' do
+ before do
+ stub_application_setting(allow_possible_spam: true)
+ end
+
+ it { is_expected.to eq(true) }
+ end
+ end
end
end
diff --git a/spec/models/concerns/taskable_spec.rb b/spec/models/concerns/taskable_spec.rb
index 140f6cda51c..0ad29454ff3 100644
--- a/spec/models/concerns/taskable_spec.rb
+++ b/spec/models/concerns/taskable_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Taskable do
+RSpec.describe Taskable, feature_category: :team_planning do
using RSpec::Parameterized::TableSyntax
describe '.get_tasks' do
@@ -13,8 +13,18 @@ RSpec.describe Taskable do
- [x] Second item
* [x] First item
* [ ] Second item
+
+ <!-- a comment
+ - [ ] Item in comment, ignore
+ rest of comment -->
+
+ [ ] No-break space (U+00A0)
+ [ ] Figure space (U+2007)
+
+ ```
+ - [ ] Item in code, ignore
+ ```
+
+ [ ] Narrow no-break space (U+202F)
+ [ ] Thin space (U+2009)
MARKDOWN
diff --git a/spec/models/concerns/triggerable_hooks_spec.rb b/spec/models/concerns/triggerable_hooks_spec.rb
index 5682a189c41..28cda269458 100644
--- a/spec/models/concerns/triggerable_hooks_spec.rb
+++ b/spec/models/concerns/triggerable_hooks_spec.rb
@@ -9,6 +9,8 @@ RSpec.describe TriggerableHooks do
TestableHook.class_eval do
include TriggerableHooks # rubocop:disable RSpec/DescribedClass
triggerable_hooks [:push_hooks]
+
+ scope :executable, -> { all }
end
end
diff --git a/spec/models/container_registry/event_spec.rb b/spec/models/container_registry/event_spec.rb
index c2c494c49fb..07ac35f7b6a 100644
--- a/spec/models/container_registry/event_spec.rb
+++ b/spec/models/container_registry/event_spec.rb
@@ -116,6 +116,24 @@ RSpec.describe ContainerRegistry::Event do
subject { described_class.new(raw_event).track! }
+ shared_examples 'tracking event is sent to HLLRedisCounter with event and originator ID' do |originator_type|
+ it 'fetches the event originator based on username' do
+ count.times do
+ expect(User).to receive(:find_by_username).with(originator.username)
+ end
+
+ subject
+ end
+
+ it 'sends a tracking event to HLLRedisCounter' do
+ expect(::Gitlab::UsageDataCounters::HLLRedisCounter)
+ .to receive(:track_event).with("i_container_registry_#{event}_#{originator_type}", values: originator.id)
+ .exactly(count).time
+
+ subject
+ end
+ end
+
context 'with a respository target' do
let(:target) do
{
@@ -164,5 +182,58 @@ RSpec.describe ContainerRegistry::Event do
end
end
end
+
+ context 'with a deploy token as the actor' do
+ let!(:originator) { create(:deploy_token, username: 'username', id: 3) }
+ let(:raw_event) do
+ {
+ 'action' => 'push',
+ 'target' => { 'tag' => 'latest' },
+ 'actor' => { 'user_type' => 'deploy_token', 'name' => originator.username }
+ }
+ end
+
+ it 'does not send a tracking event to HLLRedisCounter' do
+ expect(::Gitlab::UsageDataCounters::HLLRedisCounter).not_to receive(:track_event)
+
+ subject
+ end
+ end
+
+ context 'with a user as the actor' do
+ let_it_be(:originator) { create(:user, username: 'username') }
+ let(:raw_event) do
+ {
+ 'action' => action,
+ 'target' => target,
+ 'actor' => { 'user_type' => user_type, 'name' => originator.username }
+ }
+ end
+
+ where(:target, :action, :event, :user_type, :count) do
+ { 'tag' => 'latest' } | 'push' | 'push_tag' | 'personal_access_token' | 1
+ { 'tag' => 'latest' } | 'delete' | 'delete_tag' | 'personal_access_token' | 1
+ { 'repository' => 'foo/bar' } | 'push' | 'create_repository' | 'build' | 1
+ { 'repository' => 'foo/bar' } | 'delete' | 'delete_repository' | 'gitlab_or_ldap' | 1
+ { 'repository' => 'foo/bar' } | 'delete' | 'delete_repository' | 'not_a_user' | 0
+ { 'tag' => 'latest' } | 'copy' | '' | nil | 0
+ { 'repository' => 'foo/bar' } | 'copy' | '' | '' | 0
+ end
+
+ with_them do
+ it_behaves_like 'tracking event is sent to HLLRedisCounter with event and originator ID', :user
+ end
+ end
+
+ context 'without an actor name' do
+ let(:raw_event) { { 'action' => 'push', 'target' => {}, 'actor' => { 'user_type' => 'personal_access_token' } } }
+
+ it 'does not send a tracking event to HLLRedisCounter' do
+ expect(User).not_to receive(:find_by_username)
+ expect(::Gitlab::UsageDataCounters::HLLRedisCounter).not_to receive(:track_event)
+
+ subject
+ end
+ end
end
end
diff --git a/spec/models/container_repository_spec.rb b/spec/models/container_repository_spec.rb
index 33d3cabb325..da7b54644bd 100644
--- a/spec/models/container_repository_spec.rb
+++ b/spec/models/container_repository_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ContainerRepository, :aggregate_failures do
+RSpec.describe ContainerRepository, :aggregate_failures, feature_category: :container_registry do
using RSpec::Parameterized::TableSyntax
let(:group) { create(:group, name: 'group') }
diff --git a/spec/models/cycle_analytics/project_level_stage_adapter_spec.rb b/spec/models/cycle_analytics/project_level_stage_adapter_spec.rb
index ee13aae50dc..1516de8defd 100644
--- a/spec/models/cycle_analytics/project_level_stage_adapter_spec.rb
+++ b/spec/models/cycle_analytics/project_level_stage_adapter_spec.rb
@@ -10,11 +10,14 @@ RSpec.describe CycleAnalytics::ProjectLevelStageAdapter, type: :model do
end
end
- let_it_be(:project) { merge_request.target_project }
+ let_it_be(:project) { merge_request.target_project.reload }
let(:stage) do
- params = Gitlab::Analytics::CycleAnalytics::DefaultStages.find_by_name!(stage_name).merge(project: project)
- Analytics::CycleAnalytics::ProjectStage.new(params)
+ params = Gitlab::Analytics::CycleAnalytics::DefaultStages
+ .find_by_name!(stage_name)
+ .merge(namespace: project.project_namespace)
+
+ Analytics::CycleAnalytics::Stage.new(params)
end
around do |example|
diff --git a/spec/models/deploy_key_spec.rb b/spec/models/deploy_key_spec.rb
index 3272d5236d3..337fa40b4ba 100644
--- a/spec/models/deploy_key_spec.rb
+++ b/spec/models/deploy_key_spec.rb
@@ -10,6 +10,7 @@ RSpec.describe DeployKey, :mailer do
is_expected.to have_many(:deploy_keys_projects_with_write_access)
.conditions(can_push: true)
.class_name('DeployKeysProject')
+ .inverse_of(:deploy_key)
end
it do
@@ -20,7 +21,8 @@ RSpec.describe DeployKey, :mailer do
end
it { is_expected.to have_many(:projects) }
- it { is_expected.to have_many(:protected_branch_push_access_levels) }
+ it { is_expected.to have_many(:protected_branch_push_access_levels).inverse_of(:deploy_key) }
+ it { is_expected.to have_many(:protected_tag_create_access_levels).inverse_of(:deploy_key) }
end
describe 'notification' do
diff --git a/spec/models/deployment_spec.rb b/spec/models/deployment_spec.rb
index f0fdc62e6c7..46a1b4ce588 100644
--- a/spec/models/deployment_spec.rb
+++ b/spec/models/deployment_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Deployment do
+RSpec.describe Deployment, feature_category: :continuous_delivery do
subject { build(:deployment) }
it { is_expected.to belong_to(:project).required }
@@ -164,7 +164,8 @@ RSpec.describe Deployment do
freeze_time do
expect(Deployments::HooksWorker)
.to receive(:perform_async)
- .with(deployment_id: deployment.id, status: 'running', status_changed_at: Time.current)
+ .with(hash_including({ 'deployment_id' => deployment.id, 'status' => 'running',
+ 'status_changed_at' => Time.current.to_s }))
deployment.run!
end
@@ -200,8 +201,9 @@ RSpec.describe Deployment do
it 'executes Deployments::HooksWorker asynchronously' do
freeze_time do
expect(Deployments::HooksWorker)
- .to receive(:perform_async)
- .with(deployment_id: deployment.id, status: 'success', status_changed_at: Time.current)
+ .to receive(:perform_async)
+ .with(hash_including({ 'deployment_id' => deployment.id, 'status' => 'success',
+ 'status_changed_at' => Time.current.to_s }))
deployment.succeed!
end
@@ -231,7 +233,8 @@ RSpec.describe Deployment do
freeze_time do
expect(Deployments::HooksWorker)
.to receive(:perform_async)
- .with(deployment_id: deployment.id, status: 'failed', status_changed_at: Time.current)
+ .with(hash_including({ 'deployment_id' => deployment.id, 'status' => 'failed',
+ 'status_changed_at' => Time.current.to_s }))
deployment.drop!
end
@@ -261,8 +264,8 @@ RSpec.describe Deployment do
freeze_time do
expect(Deployments::HooksWorker)
.to receive(:perform_async)
- .with(deployment_id: deployment.id, status: 'canceled', status_changed_at: Time.current)
-
+ .with(hash_including({ 'deployment_id' => deployment.id, 'status' => 'canceled',
+ 'status_changed_at' => Time.current.to_s }))
deployment.cancel!
end
end
diff --git a/spec/models/design_management/design_spec.rb b/spec/models/design_management/design_spec.rb
index b0601ea3f08..57e0d1cad8b 100644
--- a/spec/models/design_management/design_spec.rb
+++ b/spec/models/design_management/design_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe DesignManagement::Design do
+RSpec.describe DesignManagement::Design, feature_category: :design_management do
include DesignManagementTestHelpers
let_it_be(:issue) { create(:issue) }
diff --git a/spec/models/discussion_spec.rb b/spec/models/discussion_spec.rb
index 7bd3c5743a6..1c9798c6d99 100644
--- a/spec/models/discussion_spec.rb
+++ b/spec/models/discussion_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Discussion do
+RSpec.describe Discussion, feature_category: :team_planning do
subject { described_class.new([first_note, second_note, third_note]) }
let(:first_note) { create(:diff_note_on_merge_request) }
@@ -70,4 +70,67 @@ RSpec.describe Discussion do
end
end
end
+
+ describe '#to_global_id' do
+ context 'with a single DiffNote discussion' do
+ it 'returns GID on Discussion class' do
+ discussion = described_class.build([first_note], merge_request)
+ discussion_id = discussion.id
+
+ expect(discussion.class.name.to_s).to eq("DiffDiscussion")
+ expect(discussion.to_global_id.to_s).to eq("gid://gitlab/Discussion/#{discussion_id}")
+ end
+ end
+
+ context 'with multiple DiffNotes discussion' do
+ it 'returns GID on Discussion class' do
+ discussion = described_class.build([first_note, second_note], merge_request)
+ discussion_id = discussion.id
+
+ expect(discussion.class.name.to_s).to eq("DiffDiscussion")
+ expect(discussion.to_global_id.to_s).to eq("gid://gitlab/Discussion/#{discussion_id}")
+ end
+ end
+
+ context 'with discussions on issue' do
+ let_it_be(:note_1, refind: true) { create(:note) }
+ let_it_be(:noteable) { note_1.noteable }
+
+ context 'with a single Note' do
+ it 'returns GID on Discussion class' do
+ discussion = described_class.build([note_1], noteable)
+ discussion_id = discussion.id
+
+ expect(discussion.class.name.to_s).to eq("IndividualNoteDiscussion")
+ expect(discussion.to_global_id.to_s).to eq("gid://gitlab/Discussion/#{discussion_id}")
+ end
+ end
+
+ context 'with multiple Notes' do
+ let_it_be(:note_1, refind: true) { create(:note, type: 'DiscussionNote') }
+ let_it_be(:note_2, refind: true) { create(:note, in_reply_to: note_1) }
+
+ it 'returns GID on Discussion class' do
+ discussion = described_class.build([note_1, note_2], noteable)
+ discussion_id = discussion.id
+
+ expect(discussion.class.name.to_s).to eq("Discussion")
+ expect(discussion.to_global_id.to_s).to eq("gid://gitlab/Discussion/#{discussion_id}")
+ end
+ end
+ end
+
+ context 'with system notes' do
+ let_it_be(:system_note, refind: true) { create(:note, system: true) }
+ let_it_be(:noteable) { system_note.noteable }
+
+ it 'returns GID on Discussion class' do
+ discussion = described_class.build([system_note], noteable)
+ discussion_id = discussion.id
+
+ expect(discussion.class.name.to_s).to eq("IndividualNoteDiscussion")
+ expect(discussion.to_global_id.to_s).to eq("gid://gitlab/Discussion/#{discussion_id}")
+ end
+ end
+ end
end
diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb
index 0d53ebdefe9..dfb7de34993 100644
--- a/spec/models/environment_spec.rb
+++ b/spec/models/environment_spec.rb
@@ -62,32 +62,31 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching, feature_categ
expect(environment).not_to be_valid
end
end
- end
-
- describe 'preloading deployment associations' do
- let!(:environment) { create(:environment, project: project) }
- associations = [:last_deployment, :last_visible_deployment, :upcoming_deployment]
- associations.concat Deployment::FINISHED_STATUSES.map { |status| "last_#{status}_deployment".to_sym }
- associations.concat Deployment::UPCOMING_STATUSES.map { |status| "last_#{status}_deployment".to_sym }
+ context 'tier' do
+ let!(:env) { build(:environment, tier: nil) }
- context 'raises error for legacy approach' do
- let!(:error_pattern) { /Preloading instance dependent scopes is not supported/ }
+ before do
+ # Disable `before_validation: :ensure_environment_tier` since it always set tier and interfere with tests.
+ # See: https://github.com/thoughtbot/shoulda/issues/178#issuecomment-1654014
- subject { described_class.preload(association_name).find_by(id: environment) }
+ allow_any_instance_of(described_class).to receive(:ensure_environment_tier).and_return(env)
+ end
- shared_examples 'raises error' do
- it do
- expect { subject }.to raise_error(error_pattern)
+ context 'presence is checked' do
+ it 'during create and update' do
+ expect(env).to validate_presence_of(:tier).on(:create)
+ expect(env).to validate_presence_of(:tier).on(:update)
end
end
- associations.each do |association|
- context association.to_s do
- let!(:association_name) { association }
-
- include_examples "raises error"
+ context 'when FF is disabled' do
+ before do
+ stub_feature_flags(validate_environment_tier_presence: false)
end
+
+ it { expect(env).to validate_presence_of(:tier).on(:create) }
+ it { expect(env).not_to validate_presence_of(:tier).on(:update) }
end
end
end
@@ -145,7 +144,7 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching, feature_categ
environment = create(:environment, name: 'gprd')
environment.update_column(:tier, nil)
- expect { environment.stop! }.to change { environment.reload.tier }.from(nil).to('production')
+ expect { environment.save! }.to change { environment.reload.tier }.from(nil).to('production')
end
it 'does not overwrite the existing environment tier' do
diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb
index f170eeb5841..931d12b7109 100644
--- a/spec/models/event_spec.rb
+++ b/spec/models/event_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Event, feature_category: :users do
+RSpec.describe Event, feature_category: :user_profile do
let_it_be_with_reload(:project) { create(:project) }
describe "Associations" do
diff --git a/spec/models/factories_spec.rb b/spec/models/factories_spec.rb
deleted file mode 100644
index d6e746986d6..00000000000
--- a/spec/models/factories_spec.rb
+++ /dev/null
@@ -1,211 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-# `:saas` is used to test `gitlab_subscription` factory.
-# It's not available on FOSS but also this very factory is not.
-RSpec.describe 'factories', :saas, :with_license, feature_category: :tooling do
- include Database::DatabaseHelpers
-
- # Used in `skipped` and indicates whether to skip any traits including the
- # plain factory.
- any = Object.new
-
- # https://gitlab.com/groups/gitlab-org/-/epics/5464 tracks the remaining
- # skipped factories or traits.
- #
- # Consider adding a code comment if a trait cannot produce a valid object.
- skipped = [
- [:audit_event, :unauthenticated],
- [:ci_build_trace_chunk, :fog_with_data],
- [:ci_job_artifact, :remote_store],
- [:ci_job_artifact, :raw],
- [:ci_job_artifact, :gzip],
- [:ci_job_artifact, :correct_checksum],
- [:dependency_proxy_blob, :remote_store],
- [:environment, :non_playable],
- [:composer_cache_file, :object_storage],
- [:debian_project_component_file, :object_storage],
- [:debian_project_distribution, :object_storage],
- [:debian_file_metadatum, :unknown],
- [:issue_customer_relations_contact, :for_contact],
- [:issue_customer_relations_contact, :for_issue],
- [:package_file, :object_storage],
- [:rpm_repository_file, :object_storage],
- [:pages_domain, :without_certificate],
- [:pages_domain, :without_key],
- [:pages_domain, :with_missing_chain],
- [:pages_domain, :with_trusted_chain],
- [:pages_domain, :with_trusted_expired_chain],
- [:pages_domain, :explicit_ecdsa],
- [:project_member, :blocked],
- [:remote_mirror, :ssh],
- [:user_preference, :only_comments],
- [:ci_pipeline_artifact, :remote_store],
- # EE
- [:dast_profile, :with_dast_site_validation],
- [:dependency_proxy_manifest, :remote_store],
- [:geo_dependency_proxy_manifest_state, any],
- [:ee_ci_build, :dependency_scanning_report],
- [:ee_ci_build, :license_scan_v1],
- [:ee_ci_job_artifact, :v1],
- [:ee_ci_job_artifact, :v1_1],
- [:ee_ci_job_artifact, :v2],
- [:ee_ci_job_artifact, :v2_1],
- [:geo_ci_secure_file_state, any],
- [:geo_dependency_proxy_blob_state, any],
- [:geo_event_log, :geo_event],
- [:geo_job_artifact_state, any],
- [:geo_lfs_object_state, any],
- [:geo_pages_deployment_state, any],
- [:geo_upload_state, any],
- [:geo_ci_secure_file_state, any],
- [:lfs_object, :checksum_failure],
- [:lfs_object, :checksummed],
- [:merge_request, :blocked],
- [:merge_request_diff, :verification_failed],
- [:merge_request_diff, :verification_succeeded],
- [:package_file, :verification_failed],
- [:package_file, :verification_succeeded],
- [:project, :with_vulnerabilities],
- [:scan_execution_policy, :with_schedule_and_agent],
- [:vulnerability, :with_cluster_image_scanning_finding],
- [:vulnerability, :with_findings],
- [:vulnerability_export, :finished]
- ].freeze
-
- shared_examples 'factory' do |factory|
- skip_any = skipped.include?([factory.name, any])
-
- describe "#{factory.name} factory" do
- it 'does not raise error when built' do
- # We use `skip` here because using `build` mostly work even if
- # factories break when creating them.
- skip 'Factory skipped linting due to legacy error' if skip_any
-
- expect { build(factory.name) }.not_to raise_error
- end
-
- it 'does not raise error when created' do
- pending 'Factory skipped linting due to legacy error' if skip_any
-
- expect { create(factory.name) }.not_to raise_error # rubocop:disable Rails/SaveBang
- end
-
- factory.definition.defined_traits.map(&:name).each do |trait_name|
- skip_trait = skip_any || skipped.include?([factory.name, trait_name.to_sym])
-
- describe "linting :#{trait_name} trait" do
- it 'does not raise error when created' do
- pending 'Trait skipped linting due to legacy error' if skip_trait
-
- expect { create(factory.name, trait_name) }.not_to raise_error
- end
- end
- end
- end
- end
-
- # FactoryDefault speed up specs by creating associations only once
- # and reuse them in other factories.
- #
- # However, for some factories we cannot use FactoryDefault because the
- # associations must be unique and cannot be reused, or the factory default
- # is being mutated.
- skip_factory_defaults = %i[
- ci_job_token_project_scope_link
- ci_subscriptions_project
- evidence
- exported_protected_branch
- fork_network_member
- group_member
- import_state
- issue_customer_relations_contact
- member_task
- merge_request_block
- milestone_release
- namespace
- project_namespace
- project_repository
- project_security_setting
- prometheus_alert
- prometheus_alert_event
- prometheus_metric
- protected_branch
- protected_branch_merge_access_level
- protected_branch_push_access_level
- protected_branch_unprotect_access_level
- protected_tag
- protected_tag_create_access_level
- release
- release_link
- self_managed_prometheus_alert_event
- shard
- users_star_project
- vulnerabilities_finding_identifier
- wiki_page
- wiki_page_meta
- ].to_set.freeze
-
- # Some factories and their corresponding models are based on
- # database views. In order to use those, we have to swap the
- # view out with a table of the same structure.
- factories_based_on_view = %i[
- postgres_index
- postgres_index_bloat_estimate
- postgres_autovacuum_activity
- ].to_set.freeze
-
- without_fd, with_fd = FactoryBot.factories
- .partition { |factory| skip_factory_defaults.include?(factory.name) }
-
- # Some EE models check licensed features so stub them.
- shared_context 'with licensed features' do
- licensed_features = %i[
- board_milestone_lists
- board_assignee_lists
- ].index_with(true)
-
- if Gitlab.jh?
- licensed_features.merge! %i[
- dingtalk_integration
- feishu_bot_integration
- ].index_with(true)
- end
-
- before do
- stub_licensed_features(licensed_features)
- end
- end
-
- include_context 'with licensed features' if Gitlab.ee?
-
- context 'with factory defaults', factory_default: :keep do
- let_it_be(:namespace) { create_default(:namespace).freeze }
- let_it_be(:project) { create_default(:project, :repository).freeze }
- let_it_be(:user) { create_default(:user).freeze }
-
- before do
- factories_based_on_view.each do |factory|
- view = build(factory).class.table_name
- view_gitlab_schema = Gitlab::Database::GitlabSchema.table_schema(view)
- Gitlab::Database.database_base_models.each_value.select do |base_model|
- connection = base_model.connection
- next unless Gitlab::Database.gitlab_schemas_for_connection(connection).include?(view_gitlab_schema)
-
- swapout_view_for_table(view, connection: connection)
- end
- end
- end
-
- with_fd.each do |factory|
- it_behaves_like 'factory', factory
- end
- end
-
- context 'without factory defaults' do
- without_fd.each do |factory|
- it_behaves_like 'factory', factory
- end
- end
-end
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index 4605c086763..0a05c558d45 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -17,6 +17,7 @@ RSpec.describe Group, feature_category: :subgroups do
it { is_expected.to have_many(:requesters).dependent(:destroy) }
it { is_expected.to have_many(:namespace_requesters) }
it { is_expected.to have_many(:members_and_requesters) }
+ it { is_expected.to have_many(:namespace_members_and_requesters) }
it { is_expected.to have_many(:project_group_links).dependent(:destroy) }
it { is_expected.to have_many(:shared_projects).through(:project_group_links) }
it { is_expected.to have_many(:notification_settings).dependent(:destroy) }
@@ -93,6 +94,34 @@ RSpec.describe Group, feature_category: :subgroups do
end
end
+ describe '#namespace_members_and_requesters' do
+ let_it_be(:group) { create(:group) }
+ let_it_be(:requester) { create(:user) }
+ let_it_be(:developer) { create(:user) }
+ let_it_be(:invited_member) { create(:group_member, :invited, :owner, group: group) }
+
+ before do
+ group.request_access(requester)
+ group.add_developer(developer)
+ end
+
+ it 'includes the correct users' do
+ expect(group.namespace_members_and_requesters).to include(
+ Member.find_by(user: requester),
+ Member.find_by(user: developer),
+ Member.find(invited_member.id)
+ )
+ end
+
+ it 'is equivalent to #members_and_requesters' do
+ expect(group.namespace_members_and_requesters).to match_array group.members_and_requesters
+ end
+
+ it_behaves_like 'query without source filters' do
+ subject { group.namespace_members_and_requesters }
+ end
+ end
+
shared_examples 'polymorphic membership relationship' do
it do
expect(membership.attributes).to include(
@@ -139,6 +168,24 @@ RSpec.describe Group, feature_category: :subgroups do
it_behaves_like 'member_namespace membership relationship'
end
+ describe '#namespace_members_and_requesters setters' do
+ let(:requested_at) { Time.current }
+ let(:user) { create(:user) }
+ let(:membership) do
+ group.namespace_members_and_requesters.create!(
+ user: user, requested_at: requested_at, access_level: Gitlab::Access::DEVELOPER
+ )
+ end
+
+ it { expect(membership).to be_instance_of(GroupMember) }
+ it { expect(membership.user).to eq user }
+ it { expect(membership.group).to eq group }
+ it { expect(membership.requested_at).to eq requested_at }
+
+ it_behaves_like 'polymorphic membership relationship'
+ it_behaves_like 'member_namespace membership relationship'
+ end
+
describe '#members & #requesters' do
let_it_be(:requester) { create(:user) }
let_it_be(:developer) { create(:user) }
@@ -422,7 +469,6 @@ RSpec.describe Group, feature_category: :subgroups do
before do
group.save!
- group.reload
end
it { expect(group.traversal_ids).to eq [group.id] }
@@ -434,7 +480,6 @@ RSpec.describe Group, feature_category: :subgroups do
before do
group.save!
- reload_models(parent, group)
end
it { expect(parent.traversal_ids).to eq [parent.id] }
@@ -449,7 +494,6 @@ RSpec.describe Group, feature_category: :subgroups do
before do
parent.update!(parent: new_grandparent)
group.save!
- reload_models(parent, group)
end
it 'avoid traversal_ids race condition' do
@@ -487,7 +531,6 @@ RSpec.describe Group, feature_category: :subgroups do
new_parent.update!(parent: new_grandparent)
group.save!
- reload_models(parent, group, new_grandparent, new_parent)
end
it 'avoids traversal_ids race condition' do
@@ -509,14 +552,13 @@ RSpec.describe Group, feature_category: :subgroups do
end
context 'within the same hierarchy' do
- let!(:root) { create(:group).reload }
+ let!(:root) { create(:group) }
let!(:old_parent) { create(:group, parent: root) }
let!(:new_parent) { create(:group, parent: root) }
context 'with FOR NO KEY UPDATE lock' do
before do
subject
- reload_models(old_parent, new_parent, group)
end
it 'updates traversal_ids' do
@@ -537,7 +579,6 @@ RSpec.describe Group, feature_category: :subgroups do
before do
subject
- reload_models(old_parent, new_parent, group)
end
it 'updates traversal_ids' do
@@ -567,7 +608,6 @@ RSpec.describe Group, feature_category: :subgroups do
before do
subject
- reload_models(old_parent, new_parent, group)
end
it 'updates traversal_ids' do
@@ -589,7 +629,6 @@ RSpec.describe Group, feature_category: :subgroups do
before do
subject
- reload_models(old_parent, new_parent, group)
end
it 'updates traversal_ids' do
@@ -614,11 +653,12 @@ RSpec.describe Group, feature_category: :subgroups do
before do
parent_group.update!(parent: new_grandparent)
+ reload_models(parent_group, group)
end
it 'updates traversal_ids for all descendants' do
- expect(parent_group.reload.traversal_ids).to eq [new_grandparent.id, parent_group.id]
- expect(group.reload.traversal_ids).to eq [new_grandparent.id, parent_group.id, group.id]
+ expect(parent_group.traversal_ids).to eq [new_grandparent.id, parent_group.id]
+ expect(group.traversal_ids).to eq [new_grandparent.id, parent_group.id, group.id]
end
end
end
@@ -1006,23 +1046,15 @@ RSpec.describe Group, feature_category: :subgroups do
describe '#add_user' do
let(:user) { create(:user) }
- it 'adds the user with a blocking refresh by default' do
+ it 'adds the user' do
expect_next_instance_of(GroupMember) do |member|
- expect(member).to receive(:refresh_member_authorized_projects).with(blocking: true)
+ expect(member).to receive(:refresh_member_authorized_projects).and_call_original
end
group.add_member(user, GroupMember::MAINTAINER)
expect(group.group_members.maintainers.map(&:user)).to include(user)
end
-
- it 'passes the blocking refresh value to member' do
- expect_next_instance_of(GroupMember) do |member|
- expect(member).to receive(:refresh_member_authorized_projects).with(blocking: false)
- end
-
- group.add_member(user, GroupMember::MAINTAINER, blocking_refresh: false)
- end
end
describe '#add_users' do
@@ -2913,6 +2945,22 @@ RSpec.describe Group, feature_category: :subgroups do
end
end
+ describe "#access_level_roles" do
+ let(:group) { create(:group) }
+
+ it "returns the correct roles" do
+ expect(group.access_level_roles).to eq(
+ {
+ 'Guest' => 10,
+ 'Reporter' => 20,
+ 'Developer' => 30,
+ 'Maintainer' => 40,
+ 'Owner' => 50
+ }
+ )
+ end
+ end
+
describe '#membership_locked?' do
it 'returns false' do
expect(build(:group)).not_to be_membership_locked
@@ -3557,13 +3605,6 @@ RSpec.describe Group, feature_category: :subgroups do
end
end
- describe '#work_items_create_from_markdown_feature_flag_enabled?' do
- it_behaves_like 'checks self and root ancestor feature flag' do
- let(:feature_flag) { :work_items_create_from_markdown }
- let(:feature_flag_method) { :work_items_create_from_markdown_feature_flag_enabled? }
- end
- end
-
describe 'group shares' do
let!(:sub_group) { create(:group, parent: group) }
let!(:sub_sub_group) { create(:group, parent: sub_group) }
@@ -3676,4 +3717,28 @@ RSpec.describe Group, feature_category: :subgroups do
end
end
end
+
+ describe '#readme_project' do
+ it 'returns groups project containing metadata' do
+ readme_project = create(:project, path: Group::README_PROJECT_PATH, namespace: group)
+ create(:project, namespace: group)
+
+ expect(group.readme_project).to eq(readme_project)
+ end
+ end
+
+ describe '#group_readme' do
+ it 'returns readme from group readme project' do
+ create(:project, :repository, path: Group::README_PROJECT_PATH, namespace: group)
+
+ expect(group.group_readme.name).to eq('README.md')
+ expect(group.group_readme.data).to include('testme')
+ end
+
+ it 'returns nil if no readme project is present' do
+ create(:project, :repository, namespace: group)
+
+ expect(group.group_readme).to be(nil)
+ end
+ end
end
diff --git a/spec/models/hooks/project_hook_spec.rb b/spec/models/hooks/project_hook_spec.rb
index 3d8c377ab21..c3484c4a42c 100644
--- a/spec/models/hooks/project_hook_spec.rb
+++ b/spec/models/hooks/project_hook_spec.rb
@@ -2,7 +2,19 @@
require 'spec_helper'
-RSpec.describe ProjectHook do
+RSpec.describe ProjectHook, feature_category: :integrations do
+ include_examples 'a hook that gets automatically disabled on failure' do
+ let_it_be(:project) { create(:project) }
+
+ let(:hook) { build(:project_hook, project: project) }
+ let(:hook_factory) { :project_hook }
+ let(:default_factory_arguments) { { project: project } }
+
+ def find_hooks
+ project.hooks
+ end
+ end
+
describe 'associations' do
it { is_expected.to belong_to :project }
end
@@ -67,17 +79,91 @@ RSpec.describe ProjectHook do
end
describe '#update_last_failure', :clean_gitlab_redis_shared_state do
- let(:hook) { build(:project_hook) }
+ let_it_be(:hook) { create(:project_hook) }
+
+ def last_failure
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.get(hook.project.last_failure_redis_key)
+ end
+ end
+
+ def any_failed?
+ Gitlab::Redis::SharedState.with do |redis|
+ Gitlab::Utils.to_boolean(redis.get(hook.project.web_hook_failure_redis_key))
+ end
+ end
it 'is a method of this class' do
expect { hook.update_last_failure }.not_to raise_error
end
context 'when the hook is executable' do
- it 'does not update the state' do
- expect(Gitlab::Redis::SharedState).not_to receive(:with)
+ let(:redis_key) { hook.project.web_hook_failure_redis_key }
+
+ def redis_value
+ any_failed?
+ end
+
+ context 'when the state was previously failing' do
+ before do
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.set(redis_key, true)
+ end
+ end
+
+ it 'does update the state' do
+ expect { hook.update_last_failure }.to change { redis_value }.to(false)
+ end
+
+ context 'when there is another failing sibling hook' do
+ before do
+ create(:project_hook, :permanently_disabled, project: hook.project)
+ end
+
+ it 'does not update the state' do
+ expect { hook.update_last_failure }.not_to change { redis_value }.from(true)
+ end
+
+ it 'caches the current value' do
+ Gitlab::Redis::SharedState.with do |redis|
+ expect(redis).to receive(:set).with(redis_key, 'true', ex: 1.hour).and_call_original
+ end
+
+ hook.update_last_failure
+ end
+ end
+ end
+
+ context 'when the state was previously unknown' do
+ before do
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.del(redis_key)
+ end
+ end
+
+ it 'does not update the state' do
+ expect { hook.update_last_failure }.not_to change { redis_value }.from(nil)
+ end
+ end
+
+ context 'when the state was previously not failing' do
+ before do
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.set(redis_key, false)
+ end
+ end
- hook.update_last_failure
+ it 'does not update the state' do
+ expect { hook.update_last_failure }.not_to change { redis_value }.from(false)
+ end
+
+ it 'does not cache the current value' do
+ Gitlab::Redis::SharedState.with do |redis|
+ expect(redis).not_to receive(:set)
+ end
+
+ hook.update_last_failure
+ end
end
end
@@ -86,28 +172,34 @@ RSpec.describe ProjectHook do
allow(hook).to receive(:executable?).and_return(false)
end
- def last_failure
- Gitlab::Redis::SharedState.with do |redis|
- redis.get("web_hooks:last_failure:project-#{hook.project.id}")
- end
- end
-
context 'there is no prior value', :freeze_time do
- it 'updates the state' do
+ it 'updates last_failure' do
expect { hook.update_last_failure }.to change { last_failure }.to(Time.current)
end
+
+ it 'updates any_failed?' do
+ expect { hook.update_last_failure }.to change { any_failed? }.to(true)
+ end
end
- context 'there is a prior value, from before now' do
+ context 'when there is a prior last_failure, from before now' do
it 'updates the state' do
the_future = 1.minute.from_now
-
hook.update_last_failure
travel_to(the_future) do
expect { hook.update_last_failure }.to change { last_failure }.to(the_future.iso8601)
end
end
+
+ it 'does not change the failing state' do
+ the_future = 1.minute.from_now
+ hook.update_last_failure
+
+ travel_to(the_future) do
+ expect { hook.update_last_failure }.not_to change { any_failed? }.from(true)
+ end
+ end
end
context 'there is a prior value, from after now' do
diff --git a/spec/models/hooks/service_hook_spec.rb b/spec/models/hooks/service_hook_spec.rb
index 2ece04c7158..e52af4a32b0 100644
--- a/spec/models/hooks/service_hook_spec.rb
+++ b/spec/models/hooks/service_hook_spec.rb
@@ -2,7 +2,17 @@
require 'spec_helper'
-RSpec.describe ServiceHook do
+RSpec.describe ServiceHook, feature_category: :integrations do
+ it_behaves_like 'a hook that does not get automatically disabled on failure' do
+ let(:hook) { create(:service_hook) }
+ let(:hook_factory) { :service_hook }
+ let(:default_factory_arguments) { {} }
+
+ def find_hooks
+ described_class.all
+ end
+ end
+
describe 'associations' do
it { is_expected.to belong_to :integration }
end
@@ -11,32 +21,6 @@ RSpec.describe ServiceHook do
it { is_expected.to validate_presence_of(:integration) }
end
- describe 'executable?' do
- let!(:hooks) 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],
- [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(:service_hook, recent_failures: recent_failures, disabled_until: disabled_until)
- end
- end
-
- it 'is always true' do
- expect(hooks).to all(be_executable)
- end
- end
-
describe 'execute' do
let(:hook) { build(:service_hook) }
let(:data) { { key: 'value' } }
diff --git a/spec/models/hooks/system_hook_spec.rb b/spec/models/hooks/system_hook_spec.rb
index ba94730b1dd..edb307148b6 100644
--- a/spec/models/hooks/system_hook_spec.rb
+++ b/spec/models/hooks/system_hook_spec.rb
@@ -2,7 +2,17 @@
require "spec_helper"
-RSpec.describe SystemHook do
+RSpec.describe SystemHook, feature_category: :integrations do
+ it_behaves_like 'a hook that does not get automatically disabled on failure' do
+ let(:hook) { create(:system_hook) }
+ let(:hook_factory) { :system_hook }
+ let(:default_factory_arguments) { {} }
+
+ def find_hooks
+ described_class.all
+ end
+ end
+
context 'default attributes' do
let(:system_hook) { described_class.new }
diff --git a/spec/models/hooks/web_hook_log_spec.rb b/spec/models/hooks/web_hook_log_spec.rb
index 2f0bfbd4fed..5be2b2d3bb0 100644
--- a/spec/models/hooks/web_hook_log_spec.rb
+++ b/spec/models/hooks/web_hook_log_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe WebHookLog do
+RSpec.describe WebHookLog, feature_category: :integrations do
it { is_expected.to belong_to(:web_hook) }
it { is_expected.to serialize(:request_headers).as(Hash) }
@@ -94,6 +94,35 @@ RSpec.describe WebHookLog do
end
end
+ describe 'before_save' do
+ describe '#set_url_hash' do
+ let(:web_hook_log) { build(:web_hook_log, interpolated_url: interpolated_url) }
+
+ subject(:save_web_hook_log) { web_hook_log.save! }
+
+ context 'when interpolated_url is nil' do
+ let(:interpolated_url) { nil }
+
+ it { expect { save_web_hook_log }.not_to change { web_hook_log.url_hash } }
+ end
+
+ context 'when interpolated_url has a blank value' do
+ let(:interpolated_url) { ' ' }
+
+ it { expect { save_web_hook_log }.not_to change { web_hook_log.url_hash } }
+ end
+
+ context 'when interpolated_url has a value' do
+ let(:interpolated_url) { 'example@gitlab.com' }
+ let(:expected_value) { Gitlab::CryptoHelper.sha256(interpolated_url) }
+
+ it 'assigns correct digest value' do
+ expect { save_web_hook_log }.to change { web_hook_log.url_hash }.from(nil).to(expected_value)
+ end
+ end
+ end
+ end
+
describe '.delete_batch_for' do
let_it_be(:hook) { build(:project_hook) }
let_it_be(:hook2) { build(:project_hook) }
diff --git a/spec/models/hooks/web_hook_spec.rb b/spec/models/hooks/web_hook_spec.rb
index 75ff917c036..72958a54e10 100644
--- a/spec/models/hooks/web_hook_spec.rb
+++ b/spec/models/hooks/web_hook_spec.rb
@@ -258,7 +258,7 @@ RSpec.describe WebHook, feature_category: :integrations do
end
describe 'encrypted attributes' do
- subject { described_class.encrypted_attributes.keys }
+ subject { described_class.attr_encrypted_attributes.keys }
it { is_expected.to contain_exactly(:token, :url, :url_variables) }
end
@@ -311,88 +311,6 @@ RSpec.describe WebHook, feature_category: :integrations do
end
end
- describe '.executable/.disabled' 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
- expect(described_class.where(project_id: project.id).disabled).to match_array not_executable
- end
- end
-
- describe '#executable?' do
- let_it_be_with_reload(:web_hook) { create(:project_hook, project: project) }
-
- where(:recent_failures, :not_until, :executable) do
- [
- [0, :not_set, true],
- [0, :past, true],
- [0, :future, true],
- [0, :now, true],
- [1, :not_set, true],
- [1, :past, true],
- [1, :future, true],
- [3, :not_set, true],
- [3, :past, true],
- [3, :future, true],
- [4, :not_set, false],
- [4, :past, true], # expired suspension
- [4, :now, false], # active suspension
- [4, :future, false] # active suspension
- ]
- 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
- end
- end
-
describe '#next_backoff' do
context 'when there was no last backoff' do
before do
@@ -435,50 +353,112 @@ RSpec.describe WebHook, feature_category: :integrations do
end
end
- shared_examples 'is tolerant of invalid records' do
- specify do
- hook.url = nil
+ describe '#rate_limited?' do
+ it 'is false when hook has not been rate limited' do
+ expect_next_instance_of(Gitlab::WebHooks::RateLimiter) do |rate_limiter|
+ expect(rate_limiter).to receive(:rate_limited?).and_return(false)
+ end
- expect(hook).to be_invalid
- run_expectation
+ expect(hook).not_to be_rate_limited
+ end
+
+ it 'is true when hook has been rate limited' do
+ expect_next_instance_of(Gitlab::WebHooks::RateLimiter) do |rate_limiter|
+ expect(rate_limiter).to receive(:rate_limited?).and_return(true)
+ end
+
+ expect(hook).to be_rate_limited
end
end
- describe '#enable!' do
- it 'makes a hook executable if it was marked as failed' do
- hook.recent_failures = 1000
+ describe '#rate_limit' do
+ it 'returns the hook rate limit' do
+ expect_next_instance_of(Gitlab::WebHooks::RateLimiter) do |rate_limiter|
+ expect(rate_limiter).to receive(:limit).and_return(10)
+ end
- expect { hook.enable! }.to change(hook, :executable?).from(false).to(true)
+ expect(hook.rate_limit).to eq(10)
end
+ end
- it 'makes a hook executable if it is currently backed off' do
- hook.recent_failures = 1000
- hook.disabled_until = 1.hour.from_now
+ describe '#to_json' do
+ it 'does not error' do
+ expect { hook.to_json }.not_to raise_error
+ end
- expect { hook.enable! }.to change(hook, :executable?).from(false).to(true)
+ it 'does not contain binary attributes' do
+ expect(hook.to_json).not_to include('encrypted_url_variables')
end
+ end
- it 'does not update hooks unless necessary' do
- sql_count = ActiveRecord::QueryRecorder.new { hook.enable! }.count
+ describe '#interpolated_url' do
+ subject(:hook) { build(:project_hook, project: project) }
- expect(sql_count).to eq(0)
+ context 'when the hook URL does not contain variables' do
+ before do
+ hook.url = 'http://example.com'
+ end
+
+ it { is_expected.to have_attributes(interpolated_url: hook.url) }
+ end
+
+ it 'is not vulnerable to malicious input' do
+ hook.url = 'something%{%<foo>2147483628G}'
+ hook.url_variables = { 'foo' => '1234567890.12345678' }
+
+ expect(hook).to have_attributes(interpolated_url: hook.url)
end
- include_examples 'is tolerant of invalid records' do
- def run_expectation
- hook.recent_failures = 1000
+ context 'when the hook URL contains variables' do
+ before do
+ hook.url = 'http://example.com/{path}/resource?token={token}'
+ hook.url_variables = { 'path' => 'abc', 'token' => 'xyz' }
+ end
+
+ it { is_expected.to have_attributes(interpolated_url: 'http://example.com/abc/resource?token=xyz') }
+
+ context 'when a variable is missing' do
+ before do
+ hook.url_variables = { 'path' => 'present' }
+ end
+
+ it 'raises an error' do
+ # We expect validations to prevent this entirely - this is not user-error
+ expect { hook.interpolated_url }
+ .to raise_error(described_class::InterpolationError, include('Missing key token'))
+ end
+ end
+
+ context 'when the URL appears to include percent formatting' do
+ before do
+ hook.url = 'http://example.com/%{path}/resource?token=%{token}'
+ end
- expect { hook.enable! }.to change(hook, :executable?).from(false).to(true)
+ it 'succeeds, interpolates correctly' do
+ expect(hook.interpolated_url).to eq 'http://example.com/%abc/resource?token=%xyz'
+ end
end
end
end
+ describe '#update_last_failure' do
+ it 'is a method of this class' do
+ expect { described_class.new(project: project).update_last_failure }.not_to raise_error
+ end
+ end
+
+ describe '#masked_token' do
+ it { expect(hook.masked_token).to be_nil }
+
+ context 'with a token' do
+ let(:hook) { build(:project_hook, :token, project: project) }
+
+ it { expect(hook.masked_token).to eq described_class::SECRET_MASK }
+ end
+ end
+
describe '#backoff!' do
context 'when we have not backed off before' do
- it 'does not disable the hook' do
- expect { hook.backoff! }.not_to change(hook, :executable?).from(true)
- end
-
it 'increments the recent_failures count' do
expect { hook.backoff! }.to change(hook, :recent_failures).by(1)
end
@@ -517,20 +497,6 @@ RSpec.describe WebHook, feature_category: :integrations do
expect { hook.backoff! }.to change(hook, :backoff_count).by(1)
end
- context 'when the hook is permanently disabled' do
- before do
- allow(hook).to receive(:permanently_disabled?).and_return(true)
- end
-
- it 'does not set disabled_until' do
- expect { hook.backoff! }.not_to change(hook, :disabled_until)
- end
-
- it 'does not increment the backoff count' do
- expect { hook.backoff! }.not_to change(hook, :backoff_count)
- end
- end
-
context 'when we have backed off MAX_FAILURES times' do
before do
stub_const("#{described_class}::MAX_FAILURES", 5)
@@ -554,12 +520,6 @@ RSpec.describe WebHook, feature_category: :integrations do
end
end
end
-
- include_examples 'is tolerant of invalid records' do
- def run_expectation
- expect { hook.backoff! }.to change(hook, :backoff_count).by(1)
- end
- end
end
end
@@ -585,193 +545,5 @@ RSpec.describe WebHook, feature_category: :integrations do
expect(sql_count).to eq(0)
end
-
- include_examples 'is tolerant of invalid records' do
- def run_expectation
- expect { hook.failed! }.to change(hook, :recent_failures).by(1)
- end
- end
- end
-
- describe '#disable!' do
- it 'disables a hook' do
- expect { hook.disable! }.to change(hook, :executable?).from(true).to(false)
- end
-
- include_examples 'is tolerant of invalid records' do
- def run_expectation
- expect { hook.disable! }.to change(hook, :executable?).from(true).to(false)
- end
- end
- end
-
- describe '#temporarily_disabled?' do
- it 'is false when not temporarily disabled' do
- expect(hook).not_to be_temporarily_disabled
- end
-
- it 'allows FAILURE_THRESHOLD initial failures before we back-off' do
- described_class::FAILURE_THRESHOLD.times do
- hook.backoff!
- expect(hook).not_to be_temporarily_disabled
- end
-
- hook.backoff!
- expect(hook).to be_temporarily_disabled
- end
-
- context 'when hook has been told to back off' do
- before do
- hook.update!(recent_failures: described_class::FAILURE_THRESHOLD)
- hook.backoff!
- end
-
- it 'is true' do
- expect(hook).to be_temporarily_disabled
- end
- end
- end
-
- describe '#permanently_disabled?' do
- it 'is false when not disabled' do
- expect(hook).not_to be_permanently_disabled
- end
-
- context 'when hook has been disabled' do
- before do
- hook.disable!
- end
-
- it 'is true' do
- expect(hook).to be_permanently_disabled
- end
- end
- end
-
- describe '#rate_limited?' do
- it 'is false when hook has not been rate limited' do
- expect_next_instance_of(Gitlab::WebHooks::RateLimiter) do |rate_limiter|
- expect(rate_limiter).to receive(:rate_limited?).and_return(false)
- end
-
- expect(hook).not_to be_rate_limited
- end
-
- it 'is true when hook has been rate limited' do
- expect_next_instance_of(Gitlab::WebHooks::RateLimiter) do |rate_limiter|
- expect(rate_limiter).to receive(:rate_limited?).and_return(true)
- end
-
- expect(hook).to be_rate_limited
- end
- end
-
- describe '#rate_limit' do
- it 'returns the hook rate limit' do
- expect_next_instance_of(Gitlab::WebHooks::RateLimiter) do |rate_limiter|
- expect(rate_limiter).to receive(:limit).and_return(10)
- end
-
- expect(hook.rate_limit).to eq(10)
- end
- end
-
- describe '#alert_status' do
- subject(:status) { hook.alert_status }
-
- it { is_expected.to eq :executable }
-
- context 'when hook has been disabled' do
- before do
- hook.disable!
- end
-
- it { is_expected.to eq :disabled }
- end
-
- context 'when hook has been backed off' do
- before do
- hook.update!(recent_failures: described_class::FAILURE_THRESHOLD + 1)
- hook.disabled_until = 1.hour.from_now
- end
-
- it { is_expected.to eq :temporarily_disabled }
- end
- end
-
- describe '#to_json' do
- it 'does not error' do
- expect { hook.to_json }.not_to raise_error
- end
-
- it 'does not contain binary attributes' do
- expect(hook.to_json).not_to include('encrypted_url_variables')
- end
- end
-
- describe '#interpolated_url' do
- subject(:hook) { build(:project_hook, project: project) }
-
- context 'when the hook URL does not contain variables' do
- before do
- hook.url = 'http://example.com'
- end
-
- it { is_expected.to have_attributes(interpolated_url: hook.url) }
- end
-
- it 'is not vulnerable to malicious input' do
- hook.url = 'something%{%<foo>2147483628G}'
- hook.url_variables = { 'foo' => '1234567890.12345678' }
-
- expect(hook).to have_attributes(interpolated_url: hook.url)
- end
-
- context 'when the hook URL contains variables' do
- before do
- hook.url = 'http://example.com/{path}/resource?token={token}'
- hook.url_variables = { 'path' => 'abc', 'token' => 'xyz' }
- end
-
- it { is_expected.to have_attributes(interpolated_url: 'http://example.com/abc/resource?token=xyz') }
-
- context 'when a variable is missing' do
- before do
- hook.url_variables = { 'path' => 'present' }
- end
-
- it 'raises an error' do
- # We expect validations to prevent this entirely - this is not user-error
- expect { hook.interpolated_url }
- .to raise_error(described_class::InterpolationError, include('Missing key token'))
- end
- end
-
- context 'when the URL appears to include percent formatting' do
- before do
- hook.url = 'http://example.com/%{path}/resource?token=%{token}'
- end
-
- it 'succeeds, interpolates correctly' do
- expect(hook.interpolated_url).to eq 'http://example.com/%abc/resource?token=%xyz'
- end
- end
- end
- end
-
- describe '#update_last_failure' do
- it 'is a method of this class' do
- expect { described_class.new.update_last_failure }.not_to raise_error
- end
- end
-
- describe '#masked_token' do
- it { expect(hook.masked_token).to be_nil }
-
- context 'with a token' do
- let(:hook) { build(:project_hook, :token, project: project) }
-
- it { expect(hook.masked_token).to eq described_class::SECRET_MASK }
- end
end
end
diff --git a/spec/models/incident_management/timeline_event_tag_spec.rb b/spec/models/incident_management/timeline_event_tag_spec.rb
index 1ec4fa30fb5..0f2f4e5ce9f 100644
--- a/spec/models/incident_management/timeline_event_tag_spec.rb
+++ b/spec/models/incident_management/timeline_event_tag_spec.rb
@@ -36,8 +36,16 @@ RSpec.describe IncidentManagement::TimelineEventTag do
end
describe 'constants' do
- it { expect(described_class::START_TIME_TAG_NAME).to eq('Start time') }
- it { expect(described_class::END_TIME_TAG_NAME).to eq('End time') }
+ it 'contains predefined tags' do
+ expect(described_class::PREDEFINED_TAGS).to contain_exactly(
+ 'Start time',
+ 'End time',
+ 'Impact detected',
+ 'Response initiated',
+ 'Impact mitigated',
+ 'Cause identified'
+ )
+ end
end
describe '#by_names scope' do
diff --git a/spec/models/integration_spec.rb b/spec/models/integration_spec.rb
index 9b3250e3c08..7af96c7025a 100644
--- a/spec/models/integration_spec.rb
+++ b/spec/models/integration_spec.rb
@@ -1050,9 +1050,11 @@ RSpec.describe Integration do
expect(hash['encrypted_properties']).not_to eq(record.encrypted_properties)
expect(hash['encrypted_properties_iv']).not_to eq(record.encrypted_properties_iv)
- decrypted = described_class.decrypt(:properties,
- hash['encrypted_properties'],
- { iv: hash['encrypted_properties_iv'] })
+ decrypted = described_class.attr_decrypt(
+ :properties,
+ hash['encrypted_properties'],
+ { iv: hash['encrypted_properties_iv'] }
+ )
expect(decrypted).to eq db_props
end
diff --git a/spec/models/integrations/base_chat_notification_spec.rb b/spec/models/integrations/base_chat_notification_spec.rb
index 1527ffd7278..13dd9e03ab1 100644
--- a/spec/models/integrations/base_chat_notification_spec.rb
+++ b/spec/models/integrations/base_chat_notification_spec.rb
@@ -9,13 +9,33 @@ RSpec.describe Integrations::BaseChatNotification, feature_category: :integratio
describe 'validations' do
before do
- allow(subject).to receive(:activated?).and_return(true)
+ subject.active = active
+
allow(subject).to receive(:default_channel_placeholder).and_return('placeholder')
allow(subject).to receive(:webhook_help).and_return('help')
end
- it { is_expected.to validate_presence_of :webhook }
- it { is_expected.to validate_inclusion_of(:labels_to_be_notified_behavior).in_array(%w[match_any match_all]).allow_blank }
+ def build_channel_list(count)
+ (1..count).map { |i| "##{i}" }.join(',')
+ end
+
+ context 'when active' do
+ let(:active) { true }
+
+ it { is_expected.to validate_presence_of :webhook }
+ it { is_expected.to validate_inclusion_of(:labels_to_be_notified_behavior).in_array(%w[match_any match_all]).allow_blank }
+ it { is_expected.to allow_value(build_channel_list(10)).for(:push_channel) }
+ it { is_expected.not_to allow_value(build_channel_list(11)).for(:push_channel) }
+ end
+
+ context 'when inactive' do
+ let(:active) { false }
+
+ it { is_expected.not_to validate_presence_of :webhook }
+ it { is_expected.not_to validate_inclusion_of(:labels_to_be_notified_behavior).in_array(%w[match_any match_all]).allow_blank }
+ it { is_expected.to allow_value(build_channel_list(10)).for(:push_channel) }
+ it { is_expected.to allow_value(build_channel_list(11)).for(:push_channel) }
+ end
end
describe '#execute' do
@@ -309,6 +329,10 @@ RSpec.describe Integrations::BaseChatNotification, feature_category: :integratio
context 'with multiple channel names with spaces specified' do
it_behaves_like 'with channel specified', 'slack-integration, #slack-test, @UDLP91W0A', ['slack-integration', '#slack-test', '@UDLP91W0A']
end
+
+ context 'with duplicate channel names' do
+ it_behaves_like 'with channel specified', '#slack-test,#slack-test,#slack-test-2', ['#slack-test', '#slack-test-2']
+ end
end
describe '#default_channel_placeholder' do
diff --git a/spec/models/integrations/issue_tracker_data_spec.rb b/spec/models/integrations/issue_tracker_data_spec.rb
index 233ed7b8475..285e41424c7 100644
--- a/spec/models/integrations/issue_tracker_data_spec.rb
+++ b/spec/models/integrations/issue_tracker_data_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe Integrations::IssueTrackerData do
it_behaves_like Integrations::BaseDataFields
describe 'encrypted attributes' do
- subject { described_class.encrypted_attributes.keys }
+ subject { described_class.attr_encrypted_attributes.keys }
it { is_expected.to contain_exactly(:issues_url, :new_issue_url, :project_url) }
end
diff --git a/spec/models/integrations/jira_tracker_data_spec.rb b/spec/models/integrations/jira_tracker_data_spec.rb
index d9f91527fbb..68aa30f06ed 100644
--- a/spec/models/integrations/jira_tracker_data_spec.rb
+++ b/spec/models/integrations/jira_tracker_data_spec.rb
@@ -12,7 +12,7 @@ RSpec.describe Integrations::JiraTrackerData do
end
describe 'encrypted attributes' do
- subject { described_class.encrypted_attributes.keys }
+ subject { described_class.attr_encrypted_attributes.keys }
it { is_expected.to contain_exactly(:api_url, :password, :url, :username) }
end
diff --git a/spec/models/integrations/microsoft_teams_spec.rb b/spec/models/integrations/microsoft_teams_spec.rb
index c61cc732372..4d5f4065420 100644
--- a/spec/models/integrations/microsoft_teams_spec.rb
+++ b/spec/models/integrations/microsoft_teams_spec.rb
@@ -53,7 +53,7 @@ RSpec.describe Integrations::MicrosoftTeams do
context 'with issue events' do
let(:opts) { { title: 'Awesome issue', description: 'please fix' } }
let(:issues_sample_data) do
- service = Issues::CreateService.new(project: project, current_user: user, params: opts, spam_params: nil)
+ service = Issues::CreateService.new(container: project, current_user: user, params: opts, spam_params: nil)
issue = service.execute[:issue]
service.hook_data(issue, 'open')
end
@@ -194,7 +194,7 @@ RSpec.describe Integrations::MicrosoftTeams do
end
describe 'Pipeline events' do
- let_it_be_with_reload(:project) { create(:project, :repository) }
+ let_it_be_with_refind(:project) { create(:project, :repository) }
let(:pipeline) do
create(:ci_pipeline,
diff --git a/spec/models/integrations/mock_ci_spec.rb b/spec/models/integrations/mock_ci_spec.rb
index 83954812bfe..3ff47ab2f0b 100644
--- a/spec/models/integrations/mock_ci_spec.rb
+++ b/spec/models/integrations/mock_ci_spec.rb
@@ -14,8 +14,8 @@ RSpec.describe Integrations::MockCi do
describe '#commit_status' do
let(:sha) { generate(:sha) }
- def stub_request(*args)
- WebMock.stub_request(:get, integration.commit_status_path(sha)).to_return(*args)
+ def stub_request(...)
+ WebMock.stub_request(:get, integration.commit_status_path(sha)).to_return(...)
end
def commit_status
diff --git a/spec/models/integrations/zentao_tracker_data_spec.rb b/spec/models/integrations/zentao_tracker_data_spec.rb
index dca5c4d79ae..38f2fb1b3f3 100644
--- a/spec/models/integrations/zentao_tracker_data_spec.rb
+++ b/spec/models/integrations/zentao_tracker_data_spec.rb
@@ -12,7 +12,7 @@ RSpec.describe Integrations::ZentaoTrackerData do
end
describe 'encrypted attributes' do
- subject { described_class.encrypted_attributes.keys }
+ subject { described_class.attr_encrypted_attributes.keys }
it { is_expected.to contain_exactly(:url, :api_url, :zentao_product_xid, :api_token) }
end
diff --git a/spec/models/issue_email_participant_spec.rb b/spec/models/issue_email_participant_spec.rb
index 09c231bbfda..8ddc9a5f478 100644
--- a/spec/models/issue_email_participant_spec.rb
+++ b/spec/models/issue_email_participant_spec.rb
@@ -7,6 +7,12 @@ RSpec.describe IssueEmailParticipant do
it { is_expected.to belong_to(:issue) }
end
+ describe 'Modules' do
+ subject { described_class }
+
+ it { is_expected.to include_module(Presentable) }
+ end
+
describe 'Validations' do
subject { build(:issue_email_participant) }
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index fdb397932e0..e29318a7e83 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Issue, feature_category: :project_management do
+RSpec.describe Issue, feature_category: :team_planning do
include ExternalAuthorizationServiceHelpers
using RSpec::Parameterized::TableSyntax
@@ -1465,16 +1465,6 @@ RSpec.describe Issue, feature_category: :project_management do
it 'only returns without_hidden issues' do
expect(described_class.without_hidden).to eq([public_issue])
end
-
- context 'when feature flag is disabled' do
- before do
- stub_feature_flags(ban_user_feature_flag: false)
- end
-
- it 'returns public and hidden issues' do
- expect(described_class.without_hidden).to contain_exactly(public_issue, hidden_issue)
- end
- end
end
describe '.by_project_id_and_iid' do
diff --git a/spec/models/jira_connect_installation_spec.rb b/spec/models/jira_connect_installation_spec.rb
index 525690fa6b7..6cd1534c0c8 100644
--- a/spec/models/jira_connect_installation_spec.rb
+++ b/spec/models/jira_connect_installation_spec.rb
@@ -85,14 +85,6 @@ RSpec.describe JiraConnectInstallation, feature_category: :integrations do
let(:installation) { build(:jira_connect_installation, instance_url: 'https://gitlab.example.com') }
it { is_expected.to eq('https://gitlab.example.com') }
-
- context 'and jira_connect_oauth_self_managed feature is disabled' do
- before do
- stub_feature_flags(jira_connect_oauth_self_managed: false)
- end
-
- it { is_expected.to eq('http://test.host') }
- end
end
end
diff --git a/spec/models/key_spec.rb b/spec/models/key_spec.rb
index 92f4d6d8531..f1bc7b41cee 100644
--- a/spec/models/key_spec.rb
+++ b/spec/models/key_spec.rb
@@ -489,4 +489,12 @@ RSpec.describe Key, :mailer do
end
end
end
+
+ describe '#signing?' do
+ it 'returns whether a key can be used for signing' do
+ expect(build(:key, usage_type: :signing)).to be_signing
+ expect(build(:key, usage_type: :auth_and_signing)).to be_signing
+ expect(build(:key, usage_type: :auth)).not_to be_signing
+ end
+ end
end
diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb
index 4b28f619d94..6a52f12553f 100644
--- a/spec/models/member_spec.rb
+++ b/spec/models/member_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Member do
+RSpec.describe Member, feature_category: :subgroups do
include ExclusiveLeaseHelpers
using RSpec::Parameterized::TableSyntax
@@ -891,18 +891,8 @@ RSpec.describe Member do
expect(user.authorized_projects).not_to include(project)
end
- it 'successfully completes a blocking refresh', :delete do
- expect(member).to receive(:refresh_member_authorized_projects).with(blocking: true).and_call_original
-
- member.accept_invite!(user)
-
- expect(user.authorized_projects.reload).to include(project)
- end
-
- it 'successfully completes a non-blocking refresh', :delete, :sidekiq_inline do
- member.blocking_refresh = false
-
- expect(member).to receive(:refresh_member_authorized_projects).with(blocking: false).and_call_original
+ it 'successfully completes a refresh', :delete, :sidekiq_inline do
+ expect(member).to receive(:refresh_member_authorized_projects).and_call_original
member.accept_invite!(user)
diff --git a/spec/models/members/group_member_spec.rb b/spec/models/members/group_member_spec.rb
index 4ac7ce95b84..c416e63b915 100644
--- a/spec/models/members/group_member_spec.rb
+++ b/spec/models/members/group_member_spec.rb
@@ -253,17 +253,13 @@ RSpec.describe GroupMember do
let(:action) { group.members.find_by(user: user).destroy! }
- it 'changes access level', :sidekiq_inline do
+ it 'changes access level' do
expect { action }.to change { user.can?(:guest_access, project_a) }.from(true).to(false)
.and change { user.can?(:guest_access, project_b) }.from(true).to(false)
.and change { user.can?(:guest_access, project_c) }.from(true).to(false)
end
- it 'schedules an AuthorizedProjectsWorker job to recalculate authorizations' do
- expect(AuthorizedProjectsWorker).to receive(:bulk_perform_async).with([[user.id]])
-
- action
- end
+ it_behaves_like 'calls AuthorizedProjectsWorker inline to recalculate authorizations'
end
end
end
diff --git a/spec/models/members/member_role_spec.rb b/spec/models/members/member_role_spec.rb
index b118a3c0968..4bf33eb1fce 100644
--- a/spec/models/members/member_role_spec.rb
+++ b/spec/models/members/member_role_spec.rb
@@ -78,4 +78,30 @@ RSpec.describe MemberRole, feature_category: :authentication_and_authorization d
end
end
end
+
+ describe 'callbacks' do
+ context 'for preventing deletion after member is associated' do
+ let_it_be(:member_role) { create(:member_role) }
+
+ subject(:destroy_member_role) { member_role.destroy } # rubocop: disable Rails/SaveBang
+
+ it 'allows deletion without any member associated' do
+ expect(destroy_member_role).to be_truthy
+ end
+
+ it 'prevent deletion when member is associated' do
+ create(:group_member, { group: member_role.namespace,
+ access_level: Gitlab::Access::DEVELOPER,
+ member_role: member_role })
+ member_role.members.reload
+
+ expect(destroy_member_role).to be_falsey
+ expect(member_role.errors.messages[:base])
+ .to(
+ include(s_("MemberRole|cannot be deleted because it is already assigned to a user. "\
+ "Please disassociate the member role from all users before deletion."))
+ )
+ end
+ end
+ end
end
diff --git a/spec/models/members/project_member_spec.rb b/spec/models/members/project_member_spec.rb
index d573fde5a74..f0069b89494 100644
--- a/spec/models/members/project_member_spec.rb
+++ b/spec/models/members/project_member_spec.rb
@@ -200,7 +200,8 @@ RSpec.describe ProjectMember do
end
it 'refreshes the authorization without calling AuthorizedProjectUpdate::ProjectRecalculatePerUserWorker' do
- expect(AuthorizedProjectUpdate::ProjectRecalculatePerUserWorker).not_to receive(:bulk_perform_and_wait)
+ # this is inline with the overridden behaviour in stubbed_member.rb
+ expect(AuthorizedProjectUpdate::ProjectRecalculatePerUserWorker).not_to receive(:new)
project.destroy!
end
@@ -215,8 +216,9 @@ RSpec.describe ProjectMember do
expect(project.authorized_users).not_to include(user)
end
- it 'refreshes the authorization without calling UserProjectAccessChangedService' do
- expect(UserProjectAccessChangedService).not_to receive(:new)
+ it 'refreshes the authorization without calling `AuthorizedProjectUpdate::ProjectRecalculatePerUserWorker`' do
+ # this is inline with the overridden behaviour in stubbed_member.rb
+ expect(AuthorizedProjectUpdate::ProjectRecalculatePerUserWorker).not_to receive(:new)
user.destroy!
end
@@ -224,7 +226,8 @@ RSpec.describe ProjectMember do
context 'when importing' do
it 'does not refresh' do
- expect(AuthorizedProjectUpdate::ProjectRecalculatePerUserWorker).not_to receive(:bulk_perform_and_wait)
+ # this is inline with the overridden behaviour in stubbed_member.rb
+ expect(AuthorizedProjectUpdate::ProjectRecalculatePerUserWorker).not_to receive(:new)
member = build(:project_member, project: project)
member.importing = true
@@ -250,6 +253,8 @@ RSpec.describe ProjectMember do
shared_examples_for 'calls AuthorizedProjectUpdate::UserRefreshFromReplicaWorker with a delay to update project authorizations' do
it 'calls AuthorizedProjectUpdate::UserRefreshFromReplicaWorker' do
+ stub_feature_flags(do_not_run_safety_net_auth_refresh_jobs: false)
+
expect(AuthorizedProjectUpdate::UserRefreshFromReplicaWorker).to(
receive(:bulk_perform_in)
.with(1.hour,
@@ -294,16 +299,11 @@ RSpec.describe ProjectMember do
project.add_member(user, Gitlab::Access::GUEST)
end
- it 'changes access level', :sidekiq_inline do
+ it 'changes access level' do
expect { action }.to change { user.can?(:guest_access, project) }.from(true).to(false)
end
- it 'calls AuthorizedProjectUpdate::ProjectRecalculatePerUserWorker to recalculate authorizations' do
- expect(AuthorizedProjectUpdate::ProjectRecalculatePerUserWorker).to receive(:perform_async).with(project.id, user.id)
-
- action
- end
-
+ it_behaves_like 'calls AuthorizedProjectUpdate::ProjectRecalculatePerUserWorker inline to recalculate authorizations'
it_behaves_like 'calls AuthorizedProjectUpdate::UserRefreshFromReplicaWorker with a delay to update project authorizations'
end
end
diff --git a/spec/models/merge_request/cleanup_schedule_spec.rb b/spec/models/merge_request/cleanup_schedule_spec.rb
index 1f1f33db5ed..114413e8880 100644
--- a/spec/models/merge_request/cleanup_schedule_spec.rb
+++ b/spec/models/merge_request/cleanup_schedule_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe MergeRequest::CleanupSchedule do
+RSpec.describe MergeRequest::CleanupSchedule, feature_category: :code_review_workflow do
describe 'associations' do
it { is_expected.to belong_to(:merge_request) }
end
diff --git a/spec/models/merge_request_diff_commit_spec.rb b/spec/models/merge_request_diff_commit_spec.rb
index 25e5e40feb7..78f9fb5b7d3 100644
--- a/spec/models/merge_request_diff_commit_spec.rb
+++ b/spec/models/merge_request_diff_commit_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe MergeRequestDiffCommit do
+RSpec.describe MergeRequestDiffCommit, feature_category: :code_review_workflow do
let(:merge_request) { create(:merge_request) }
let(:project) { merge_request.project }
diff --git a/spec/models/merge_request_diff_file_spec.rb b/spec/models/merge_request_diff_file_spec.rb
index 7e127caa649..eee7fe67ffb 100644
--- a/spec/models/merge_request_diff_file_spec.rb
+++ b/spec/models/merge_request_diff_file_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe MergeRequestDiffFile do
+RSpec.describe MergeRequestDiffFile, feature_category: :code_review_workflow do
it_behaves_like 'a BulkInsertSafe model', MergeRequestDiffFile do
let(:valid_items_for_bulk_insertion) do
build_list(:merge_request_diff_file, 10) do |mr_diff_file|
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index a059d5cae9b..2e2355ba710 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -135,6 +135,15 @@ RSpec.describe MergeRequest, factory_default: :keep, feature_category: :code_rev
let_it_be(:merge_request3) { create(:merge_request, :unique_branches, reviewers: []) }
let_it_be(:merge_request4) { create(:merge_request, :draft_merge_request) }
+ describe '.preload_target_project_with_namespace' do
+ subject(:mr) { described_class.preload_target_project_with_namespace.first }
+
+ it 'returns MR with the target project\'s namespace preloaded' do
+ expect(mr.association(:target_project)).to be_loaded
+ expect(mr.target_project.association(:namespace)).to be_loaded
+ end
+ end
+
describe '.review_requested' do
it 'returns MRs that have any review requests' do
expect(described_class.review_requested).to eq([merge_request1, merge_request2])
@@ -305,13 +314,41 @@ RSpec.describe MergeRequest, factory_default: :keep, feature_category: :code_rev
expect(subject).to be_valid
end
end
+
+ describe '#validate_target_project' do
+ let(:merge_request) do
+ build(:merge_request, source_project: project, target_project: project, importing: importing)
+ end
+
+ let(:project) { build_stubbed(:project) }
+ let(:importing) { false }
+
+ context 'when projects #merge_requests_enabled? is true' do
+ it { expect(merge_request.valid?(false)).to eq true }
+ end
+
+ context 'when projects #merge_requests_enabled? is false' do
+ let(:project) { build_stubbed(:project, merge_requests_enabled: false) }
+
+ it 'is invalid' do
+ expect(merge_request.valid?(false)).to eq false
+ expect(merge_request.errors.full_messages).to contain_exactly('Target project has disabled merge requests')
+ end
+
+ context 'when #import? is true' do
+ let(:importing) { true }
+
+ it { expect(merge_request.valid?(false)).to eq true }
+ end
+ end
+ end
end
describe 'callbacks' do
describe '#ensure_merge_request_diff' do
let(:merge_request) { build(:merge_request) }
- context 'when async_merge_request_diff_creation is true' do
+ context 'when skip_ensure_merge_request_diff is true' do
before do
merge_request.skip_ensure_merge_request_diff = true
end
@@ -323,7 +360,7 @@ RSpec.describe MergeRequest, factory_default: :keep, feature_category: :code_rev
end
end
- context 'when async_merge_request_diff_creation is false' do
+ context 'when skip_ensure_merge_request_diff is false' do
before do
merge_request.skip_ensure_merge_request_diff = false
end
@@ -4566,30 +4603,12 @@ RSpec.describe MergeRequest, factory_default: :keep, feature_category: :code_rev
end
describe 'transition to merged' do
- context 'when reset_merge_error_on_transition feature flag is on' do
- before do
- stub_feature_flags(reset_merge_error_on_transition: true)
- end
-
- it 'resets the merge error' do
- subject.update!(merge_error: 'temp')
+ it 'resets the merge error' do
+ subject.update!(merge_error: 'temp')
- expect { subject.mark_as_merged }.to change { subject.merge_error.present? }
- .from(true)
- .to(false)
- end
- end
-
- context 'when reset_merge_error_on_transition feature flag is off' do
- before do
- stub_feature_flags(reset_merge_error_on_transition: false)
- end
-
- it 'does not reset the merge error' do
- subject.update!(merge_error: 'temp')
-
- expect { subject.mark_as_merged }.not_to change { subject.merge_error.present? }
- end
+ expect { subject.mark_as_merged }.to change { subject.merge_error.present? }
+ .from(true)
+ .to(false)
end
end
@@ -5526,4 +5545,53 @@ RSpec.describe MergeRequest, factory_default: :keep, feature_category: :code_rev
end
end
end
+
+ describe '#diffs_batch_cache_with_max_age?' do
+ let(:merge_request) { build_stubbed(:merge_request) }
+
+ subject(:diffs_batch_cache_with_max_age?) { merge_request.diffs_batch_cache_with_max_age? }
+
+ it 'returns true' do
+ expect(diffs_batch_cache_with_max_age?).to be_truthy
+ end
+
+ context 'when diffs_batch_cache_with_max_age is disabled' do
+ before do
+ stub_feature_flags(diffs_batch_cache_with_max_age: false)
+ end
+
+ it 'returns false' do
+ expect(diffs_batch_cache_with_max_age?).to be_falsey
+ end
+ end
+ end
+
+ describe '#prepared?' do
+ subject(:merge_request) { build_stubbed(:merge_request, prepared_at: prepared_at) }
+
+ context 'when prepared_at is nil' do
+ let(:prepared_at) { nil }
+
+ it 'returns false' do
+ expect(merge_request.prepared?).to be_falsey
+ end
+ end
+
+ context 'when prepared_at is not nil' do
+ let(:prepared_at) { Time.current }
+
+ it 'returns true' do
+ expect(merge_request.prepared?).to be_truthy
+ end
+ end
+ end
+
+ describe 'prepare' do
+ it 'calls NewMergeRequestWorker' do
+ expect(NewMergeRequestWorker).to receive(:perform_async)
+ .with(subject.id, subject.author_id)
+
+ subject.prepare
+ end
+ end
end
diff --git a/spec/models/ml/candidate_spec.rb b/spec/models/ml/candidate_spec.rb
index fa8952dc0f4..374e49aea01 100644
--- a/spec/models/ml/candidate_spec.rb
+++ b/spec/models/ml/candidate_spec.rb
@@ -2,9 +2,11 @@
require 'spec_helper'
-RSpec.describe Ml::Candidate, factory_default: :keep do
- let_it_be(:candidate) { create(:ml_candidates, :with_metrics_and_params) }
- let_it_be(:candidate2) { create(:ml_candidates, experiment: candidate.experiment) }
+RSpec.describe Ml::Candidate, factory_default: :keep, feature_category: :mlops do
+ let_it_be(:candidate) { create(:ml_candidates, :with_metrics_and_params, name: 'candidate0') }
+ let_it_be(:candidate2) do
+ create(:ml_candidates, experiment: candidate.experiment, user: create(:user), name: 'candidate2')
+ end
let_it_be(:candidate_artifact) do
FactoryBot.create(:generic_package,
@@ -109,12 +111,12 @@ RSpec.describe Ml::Candidate, factory_default: :keep do
end
describe "#latest_metrics" do
- let_it_be(:candidate2) { create(:ml_candidates, experiment: candidate.experiment) }
- let!(:metric1) { create(:ml_candidate_metrics, candidate: candidate2) }
- let!(:metric2) { create(:ml_candidate_metrics, candidate: candidate2 ) }
- let!(:metric3) { create(:ml_candidate_metrics, name: metric1.name, candidate: candidate2) }
+ let_it_be(:candidate3) { create(:ml_candidates, experiment: candidate.experiment) }
+ let_it_be(:metric1) { create(:ml_candidate_metrics, candidate: candidate3) }
+ let_it_be(:metric2) { create(:ml_candidate_metrics, candidate: candidate3 ) }
+ let_it_be(:metric3) { create(:ml_candidate_metrics, name: metric1.name, candidate: candidate3) }
- subject { candidate2.latest_metrics }
+ subject { candidate3.latest_metrics }
it 'fetches only the last metric for the name' do
expect(subject).to match_array([metric2, metric3] )
@@ -130,4 +132,55 @@ RSpec.describe Ml::Candidate, factory_default: :keep do
expect(subject.association_cached?(:user)).to be(true)
end
end
+
+ describe '#by_name' do
+ let(:name) { candidate.name }
+
+ subject { described_class.by_name(name) }
+
+ context 'when name matches' do
+ it 'gets the correct candidates' do
+ expect(subject).to match_array([candidate])
+ end
+ end
+
+ context 'when name matches partially' do
+ let(:name) { 'andidate' }
+
+ it 'gets the correct candidates' do
+ expect(subject).to match_array([candidate, candidate2])
+ end
+ end
+
+ context 'when name does not match' do
+ let(:name) { non_existing_record_id.to_s }
+
+ it 'does not fetch any candidate' do
+ expect(subject).to match_array([])
+ end
+ end
+ end
+
+ describe '#order_by_metric' do
+ let_it_be(:auc_metrics) do
+ create(:ml_candidate_metrics, name: 'auc', value: 0.4, candidate: candidate)
+ create(:ml_candidate_metrics, name: 'auc', value: 0.8, candidate: candidate2)
+ end
+
+ let(:direction) { 'desc' }
+
+ subject { described_class.order_by_metric('auc', direction) }
+
+ it 'orders correctly' do
+ expect(subject).to eq([candidate2, candidate])
+ end
+
+ context 'when direction is asc' do
+ let(:direction) { 'asc' }
+
+ it 'orders correctly' do
+ expect(subject).to eq([candidate, candidate2])
+ end
+ end
+ end
end
diff --git a/spec/models/ml/experiment_spec.rb b/spec/models/ml/experiment_spec.rb
index 52e9f9217f5..c75331a2ab5 100644
--- a/spec/models/ml/experiment_spec.rb
+++ b/spec/models/ml/experiment_spec.rb
@@ -57,4 +57,21 @@ RSpec.describe Ml::Experiment do
it { is_expected.to be_empty }
end
end
+
+ describe '#with_candidate_count' do
+ let_it_be(:exp3) do
+ create(:ml_experiments, project: exp.project).tap do |e|
+ create_list(:ml_candidates, 3, experiment: e, user: nil)
+ create(:ml_candidates, experiment: exp2, user: nil)
+ end
+ end
+
+ subject { described_class.with_candidate_count.to_h { |e| [e.id, e.candidate_count] } }
+
+ it 'fetches the candidate count', :aggregate_failures do
+ expect(subject[exp.id]).to eq(0)
+ expect(subject[exp2.id]).to eq(1)
+ expect(subject[exp3.id]).to eq(3)
+ end
+ end
end
diff --git a/spec/models/namespace/traversal_hierarchy_spec.rb b/spec/models/namespace/traversal_hierarchy_spec.rb
index 918ff6aa154..b0088e44087 100644
--- a/spec/models/namespace/traversal_hierarchy_spec.rb
+++ b/spec/models/namespace/traversal_hierarchy_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Namespace::TraversalHierarchy, type: :model do
+RSpec.describe Namespace::TraversalHierarchy, type: :model, feature_category: :subgroups do
let!(:root) { create(:group, :with_hierarchy) }
describe '.for_namespace' do
diff --git a/spec/models/namespace_setting_spec.rb b/spec/models/namespace_setting_spec.rb
index 15b80749aa2..b7cc59b5af3 100644
--- a/spec/models/namespace_setting_spec.rb
+++ b/spec/models/namespace_setting_spec.rb
@@ -159,6 +159,36 @@ RSpec.describe NamespaceSetting, feature_category: :subgroups, type: :model do
end
end
+ describe '#emails_enabled?' do
+ context 'when a group has no parent'
+ let(:settings) { create(:namespace_settings, emails_enabled: true) }
+ let(:grandparent) { create(:group) }
+ let(:parent) { create(:group, parent: grandparent) }
+ let(:group) { create(:group, parent: parent, namespace_settings: settings) }
+
+ context 'when the groups setting is changed' do
+ it 'returns false when the attribute is false' do
+ group.update_attribute(:emails_disabled, true)
+
+ expect(group.emails_enabled?).to be_falsey
+ end
+ end
+
+ context 'when a group has a parent' do
+ it 'returns true when no parent has disabled emails' do
+ expect(group.emails_enabled?).to be_truthy
+ end
+
+ context 'when ancestor emails are disabled' do
+ it 'returns false' do
+ grandparent.update_attribute(:emails_disabled, true)
+
+ expect(group.emails_enabled?).to be_falsey
+ end
+ end
+ end
+ end
+
context 'when a group has parent groups' do
let(:grandparent) { create(:group, namespace_settings: settings) }
let(:parent) { create(:group, parent: grandparent) }
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index d063f4713c7..a0698ac30f5 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Namespace do
+RSpec.describe Namespace, feature_category: :subgroups do
include ProjectForksHelper
include ReloadHelpers
@@ -36,6 +36,8 @@ RSpec.describe Namespace do
it { is_expected.to have_many(:work_items) }
it { is_expected.to have_many :achievements }
it { is_expected.to have_many(:namespace_commit_emails).class_name('Users::NamespaceCommitEmail') }
+ it { is_expected.to have_many(:cycle_analytics_stages) }
+ it { is_expected.to have_many(:value_streams) }
it do
is_expected.to have_one(:ci_cd_settings).class_name('NamespaceCiCdSetting').inverse_of(:namespace).autosave(true)
@@ -402,6 +404,62 @@ RSpec.describe Namespace do
it { is_expected.to include_module(Namespaces::Traversal::LinearScopes) }
end
+ describe '#traversal_ids' do
+ let(:namespace) { build(:group) }
+
+ context 'when namespace not persisted' do
+ it 'returns []' do
+ expect(namespace.traversal_ids).to eq []
+ end
+ end
+
+ context 'when namespace just saved' do
+ let(:namespace) { build(:group) }
+
+ before do
+ namespace.save!
+ end
+
+ it 'returns value that matches database' do
+ expect(namespace.traversal_ids).to eq Namespace.find(namespace.id).traversal_ids
+ end
+ end
+
+ context 'when namespace loaded from database' do
+ before do
+ namespace.save!
+ namespace.reload
+ end
+
+ it 'returns database value' do
+ expect(namespace.traversal_ids).to eq Namespace.find(namespace.id).traversal_ids
+ end
+ end
+
+ context 'when made a child group' do
+ let!(:namespace) { create(:group) }
+ let!(:parent_namespace) { create(:group, children: [namespace]) }
+
+ it 'returns database value' do
+ expect(namespace.traversal_ids).to eq [parent_namespace.id, namespace.id]
+ end
+ end
+
+ context 'when root_ancestor changes' do
+ let(:old_root) { create(:group) }
+ let(:namespace) { create(:group, parent: old_root) }
+ let(:new_root) { create(:group) }
+
+ it 'resets root_ancestor memo' do
+ expect(namespace.root_ancestor).to eq old_root
+
+ namespace.update!(parent: new_root)
+
+ expect(namespace.root_ancestor).to eq new_root
+ end
+ end
+ end
+
context 'traversal scopes' do
context 'recursive' do
before do
@@ -477,36 +535,60 @@ RSpec.describe Namespace do
end
context 'traversal_ids on create' do
- shared_examples 'default traversal_ids' do
- let!(:namespace) { create(:group) }
- let!(:child_namespace) { create(:group, parent: namespace) }
+ let(:parent) { create(:group) }
+ let(:child) { create(:group, parent: parent) }
- it { expect(namespace.reload.traversal_ids).to eq [namespace.id] }
- it { expect(child_namespace.reload.traversal_ids).to eq [namespace.id, child_namespace.id] }
- it { expect(namespace.sync_events.count).to eq 1 }
- it { expect(child_namespace.sync_events.count).to eq 1 }
- end
+ it { expect(parent.traversal_ids).to eq [parent.id] }
+ it { expect(child.traversal_ids).to eq [parent.id, child.id] }
+ it { expect(parent.sync_events.count).to eq 1 }
+ it { expect(child.sync_events.count).to eq 1 }
- it_behaves_like 'default traversal_ids'
+ context 'when set_traversal_ids_on_save feature flag is disabled' do
+ before do
+ stub_feature_flags(set_traversal_ids_on_save: false)
+ end
+
+ it 'only sets traversal_ids on reload' do
+ expect { parent.reload }.to change(parent, :traversal_ids).from([]).to([parent.id])
+ expect { child.reload }.to change(child, :traversal_ids).from([]).to([parent.id, child.id])
+ end
+ end
end
context 'traversal_ids on update' do
- let!(:namespace1) { create(:group) }
- let!(:namespace2) { create(:group) }
+ let(:namespace1) { create(:group) }
+ let(:namespace2) { create(:group) }
+
+ context 'when parent_id is changed' do
+ subject { namespace1.update!(parent: namespace2) }
+
+ it 'sets the traversal_ids attribute' do
+ expect { subject }.to change { namespace1.traversal_ids }.from([namespace1.id]).to([namespace2.id, namespace1.id])
+ end
+
+ context 'when set_traversal_ids_on_save feature flag is disabled' do
+ before do
+ stub_feature_flags(set_traversal_ids_on_save: false)
+ end
- it 'updates the traversal_ids when the parent_id is changed' do
- expect do
- namespace1.update!(parent: namespace2)
- end.to change { namespace1.reload.traversal_ids }.from([namespace1.id]).to([namespace2.id, namespace1.id])
+ it 'sets traversal_ids after reload' do
+ subject
+
+ expect { namespace1.reload }.to change(namespace1, :traversal_ids).from([]).to([namespace2.id, namespace1.id])
+ end
+ end
end
it 'creates a Namespaces::SyncEvent using triggers' do
Namespaces::SyncEvent.delete_all
- namespace1.update!(parent: namespace2)
- expect(namespace1.reload.sync_events.count).to eq(1)
+
+ expect { namespace1.update!(parent: namespace2) }.to change(namespace1.sync_events, :count).by(1)
end
it 'creates sync_events using database trigger on the table' do
+ namespace1.save!
+ namespace2.save!
+
expect { Group.update_all(traversal_ids: [-1]) }.to change(Namespaces::SyncEvent, :count).by(2)
end
@@ -1263,12 +1345,23 @@ RSpec.describe Namespace do
end
describe ".clean_path" do
- let!(:user) { create(:user, username: "johngitlab-etc") }
- let!(:namespace) { create(:namespace, path: "JohnGitLab-etc1") }
+ it "cleans the path and makes sure it's available", time_travel_to: '2023-04-20 00:07 -0700' do
+ create :user, username: "johngitlab-etc"
+ create :namespace, path: "JohnGitLab-etc1"
+ [nil, 1, 2, 3].each do |count|
+ create :namespace, path: "pickle#{count}"
+ end
- it "cleans the path and makes sure it's available" do
expect(described_class.clean_path("-john+gitlab-ETC%.git@gmail.com")).to eq("johngitlab-ETC2")
expect(described_class.clean_path("--%+--valid_*&%name=.git.%.atom.atom.@email.com")).to eq("valid_name")
+
+ # when we have more than MAX_TRIES count of a path use a more randomized suffix
+ expect(described_class.clean_path("pickle@gmail.com")).to eq("pickle4")
+ create(:namespace, path: "pickle4")
+ expect(described_class.clean_path("pickle@gmail.com")).to eq("pickle716")
+ create(:namespace, path: "pickle716")
+ expect(described_class.clean_path("pickle@gmail.com")).to eq("pickle717")
+ expect(described_class.clean_path("--$--pickle@gmail.com")).to eq("pickle717")
end
end
@@ -1595,6 +1688,8 @@ RSpec.describe Namespace do
end
it 'calls AuthorizedProjectUpdate::UserRefreshFromReplicaWorker with a delay to update project authorizations' do
+ stub_feature_flags(do_not_run_safety_net_auth_refresh_jobs: false)
+
expect(AuthorizedProjectUpdate::UserRefreshFromReplicaWorker).to(
receive(:bulk_perform_in)
.with(1.hour,
@@ -1992,10 +2087,21 @@ RSpec.describe Namespace do
end
describe '#emails_enabled?' do
- it "is the opposite of emails_disabled" do
- group = create(:group, emails_disabled: false)
+ context 'without a persisted namespace_setting object' do
+ let(:group) { build(:group, emails_disabled: false) }
- expect(group.emails_enabled?).to be_truthy
+ it "is the opposite of emails_disabled" do
+ expect(group.emails_enabled?).to be_truthy
+ end
+ end
+
+ context 'with a persisted namespace_setting object' do
+ let(:namespace_settings) { create(:namespace_settings, emails_enabled: true) }
+ let(:group) { build(:group, emails_disabled: false, namespace_settings: namespace_settings) }
+
+ it "is the opposite of emails_disabled" do
+ expect(group.emails_enabled?).to be_truthy
+ end
end
end
diff --git a/spec/models/namespaces/randomized_suffix_path_spec.rb b/spec/models/namespaces/randomized_suffix_path_spec.rb
new file mode 100644
index 00000000000..a2484030f3c
--- /dev/null
+++ b/spec/models/namespaces/randomized_suffix_path_spec.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Namespaces::RandomizedSuffixPath, feature_category: :not_owned do
+ let(:path) { 'backintime' }
+
+ subject(:suffixed_path) { described_class.new(path) }
+
+ describe '#to_s' do
+ it 'represents with given path' do
+ expect(suffixed_path.to_s).to eq('backintime')
+ end
+ end
+
+ describe '#call' do
+ it 'returns path without count when count is 0' do
+ expect(suffixed_path.call(0)).to eq('backintime')
+ end
+
+ it "returns path suffixed with count when between 0 and #{described_class::MAX_TRIES}" do
+ (1..described_class::MAX_TRIES).each do |count|
+ expect(suffixed_path.call(count)).to eq("backintime#{count}")
+ end
+ end
+
+ it 'adds a "randomized" suffix when MAX_TRIES is exhausted', time_travel_to: '1955-11-12 06:38' do
+ count = described_class::MAX_TRIES + 1
+ expect(suffixed_path.call(count)).to eq("backintime3845")
+ end
+
+ it 'adds an offset to the "randomized" suffix when MAX_TRIES is exhausted', time_travel_to: '1955-11-12 06:38' do
+ count = described_class::MAX_TRIES + 2
+ expect(suffixed_path.call(count)).to eq("backintime3846")
+ end
+ end
+end
diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb
index 4b574540500..013070f7be5 100644
--- a/spec/models/note_spec.rb
+++ b/spec/models/note_spec.rb
@@ -1482,6 +1482,7 @@ RSpec.describe Note do
end
it "expires cache for note's issue when note is destroyed" do
+ note.save!
expect_expiration(note.noteable)
note.destroy!
@@ -1643,17 +1644,6 @@ RSpec.describe Note do
match_query_count(1).for_model(DiffNotePosition))
end
end
-
- context 'when skip_notes_diff_include flag is disabled' do
- before do
- stub_feature_flags(skip_notes_diff_include: false)
- end
-
- it 'includes additional diff associations' do
- expect { subject.reload }.to match_query_count(1).for_model(NoteDiffFile).and(
- match_query_count(1).for_model(DiffNotePosition))
- end
- end
end
context 'when noteable can have diffs' do
@@ -1889,4 +1879,34 @@ RSpec.describe Note do
it { is_expected.to eq :read_internal_note }
end
end
+
+ describe '#exportable_record?' do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project, :private) }
+ let_it_be(:noteable) { create(:issue, project: project) }
+
+ subject { note.exportable_record?(user) }
+
+ context 'when not a system note' do
+ let(:note) { build(:note, noteable: noteable) }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'with system note' do
+ let(:note) { build(:system_note, project: project, noteable: noteable) }
+
+ it 'returns `false` when the user cannot read the note' do
+ is_expected.to be_falsey
+ end
+
+ context 'when user can read the note' do
+ before do
+ project.add_developer(user)
+ end
+
+ it { is_expected.to be_truthy }
+ end
+ end
+ end
end
diff --git a/spec/models/onboarding/learn_gitlab_spec.rb b/spec/models/onboarding/learn_gitlab_spec.rb
deleted file mode 100644
index 5e3e1f9c304..00000000000
--- a/spec/models/onboarding/learn_gitlab_spec.rb
+++ /dev/null
@@ -1,69 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Onboarding::LearnGitlab do
- let_it_be(:current_user) { create(:user) }
- let_it_be(:learn_gitlab_project) { create(:project, name: described_class::PROJECT_NAME) }
- let_it_be(:learn_gitlab_board) { create(:board, project: learn_gitlab_project, name: described_class::BOARD_NAME) }
- let_it_be(:learn_gitlab_label) { create(:label, project: learn_gitlab_project, name: described_class::LABEL_NAME) }
-
- before do
- learn_gitlab_project.add_developer(current_user)
- end
-
- describe '#available?' do
- using RSpec::Parameterized::TableSyntax
-
- where(:project, :board, :label, :expected_result) do
- nil | nil | nil | nil
- nil | nil | true | nil
- nil | true | nil | nil
- nil | true | true | nil
- true | nil | nil | nil
- true | nil | true | nil
- true | true | nil | nil
- true | true | true | true
- end
-
- with_them do
- before do
- allow_next_instance_of(described_class) do |learn_gitlab|
- allow(learn_gitlab).to receive(:project).and_return(project)
- allow(learn_gitlab).to receive(:board).and_return(board)
- allow(learn_gitlab).to receive(:label).and_return(label)
- end
- end
-
- subject { described_class.new(current_user).available? }
-
- it { is_expected.to be expected_result }
- end
- end
-
- describe '#project' do
- subject { described_class.new(current_user).project }
-
- it { is_expected.to eq learn_gitlab_project }
-
- context 'when it is created during trial signup' do
- let_it_be(:learn_gitlab_project) do
- create(:project, name: described_class::PROJECT_NAME_ULTIMATE_TRIAL, path: 'learn-gitlab-ultimate-trial')
- end
-
- it { is_expected.to eq learn_gitlab_project }
- end
- end
-
- describe '#board' do
- subject { described_class.new(current_user).board }
-
- it { is_expected.to eq learn_gitlab_board }
- end
-
- describe '#label' do
- subject { described_class.new(current_user).label }
-
- it { is_expected.to eq learn_gitlab_label }
- end
-end
diff --git a/spec/models/packages/composer/metadatum_spec.rb b/spec/models/packages/composer/metadatum_spec.rb
index 1c888f1563c..326eba7aa0e 100644
--- a/spec/models/packages/composer/metadatum_spec.rb
+++ b/spec/models/packages/composer/metadatum_spec.rb
@@ -10,6 +10,35 @@ RSpec.describe Packages::Composer::Metadatum, type: :model do
it { is_expected.to validate_presence_of(:package) }
it { is_expected.to validate_presence_of(:target_sha) }
it { is_expected.to validate_presence_of(:composer_json) }
+
+ describe '#composer_package_type' do
+ subject { build(:composer_metadatum, package: package) }
+
+ shared_examples 'an invalid record' do
+ it do
+ expect(subject).not_to be_valid
+ expect(subject.errors.to_a).to include('Package type must be Composer')
+ end
+ end
+
+ context 'when the metadatum package_type is Composer' do
+ let(:package) { build(:composer_package) }
+
+ it { is_expected.to be_valid }
+ end
+
+ context 'when the metadatum has no associated package' do
+ let(:package) { nil }
+
+ it_behaves_like 'an invalid record'
+ end
+
+ context 'when the metadatum package_type is not Composer' do
+ let(:package) { build(:npm_package) }
+
+ it_behaves_like 'an invalid record'
+ end
+ end
end
describe 'scopes' do
diff --git a/spec/models/packages/debian/file_entry_spec.rb b/spec/models/packages/debian/file_entry_spec.rb
index ed6372f2873..e981adf69bc 100644
--- a/spec/models/packages/debian/file_entry_spec.rb
+++ b/spec/models/packages/debian/file_entry_spec.rb
@@ -31,13 +31,6 @@ RSpec.describe Packages::Debian::FileEntry, type: :model do
describe 'validations' do
it { is_expected.to be_valid }
- context 'with FIPS mode', :fips_mode do
- it 'raises an error' do
- expect { subject.validate! }
- .to raise_error(::Packages::FIPS::DisabledError, 'Debian registry is not FIPS compliant')
- end
- end
-
describe '#filename' do
it { is_expected.to validate_presence_of(:filename) }
it { is_expected.not_to allow_value('Hé').for(:filename) }
diff --git a/spec/models/packages/package_spec.rb b/spec/models/packages/package_spec.rb
index a8bcda1242f..992cc5c4354 100644
--- a/spec/models/packages/package_spec.rb
+++ b/spec/models/packages/package_spec.rb
@@ -33,6 +33,26 @@ RSpec.describe Packages::Package, type: :model, feature_category: :package_regis
it { is_expected.to contain_exactly(publication.package) }
end
+ describe '.with_debian_codename_or_suite' do
+ let_it_be(:distribution1) { create(:debian_project_distribution, :with_suite) }
+ let_it_be(:distribution2) { create(:debian_project_distribution, :with_suite) }
+
+ let_it_be(:package1) { create(:debian_package, published_in: distribution1) }
+ let_it_be(:package2) { create(:debian_package, published_in: distribution2) }
+
+ context 'with a codename' do
+ subject { described_class.with_debian_codename_or_suite(distribution1.codename).to_a }
+
+ it { is_expected.to contain_exactly(package1) }
+ end
+
+ context 'with a suite' do
+ subject { described_class.with_debian_codename_or_suite(distribution2.suite).to_a }
+
+ it { is_expected.to contain_exactly(package2) }
+ end
+ end
+
describe '.with_composer_target' do
let!(:package1) { create(:composer_package, :with_metadatum, sha: '123') }
let!(:package2) { create(:composer_package, :with_metadatum, sha: '123') }
@@ -1048,14 +1068,16 @@ RSpec.describe Packages::Package, type: :model, feature_category: :package_regis
let_it_be(:project) { create(:project) }
let_it_be(:package) { create(:maven_package, project: project) }
let_it_be(:package2) { create(:maven_package, project: project) }
- let_it_be(:package3) { create(:maven_package, project: project, name: 'foo') }
+ let_it_be(:package3) { create(:maven_package, :error, project: project) }
+ let_it_be(:package4) { create(:maven_package, project: project, name: 'foo') }
+ let_it_be(:pending_destruction_package) { create(:maven_package, :pending_destruction, project: project) }
it 'returns other package versions of the same package name belonging to the project' do
- expect(package.versions).to contain_exactly(package2)
+ expect(package.versions).to contain_exactly(package2, package3)
end
it 'does not return different packages' do
- expect(package.versions).not_to include(package3)
+ expect(package.versions).not_to include(package4)
end
end
diff --git a/spec/models/packages/tag_spec.rb b/spec/models/packages/tag_spec.rb
index 842ba7ad518..bc03c34f56b 100644
--- a/spec/models/packages/tag_spec.rb
+++ b/spec/models/packages/tag_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require 'spec_helper'
-RSpec.describe Packages::Tag, type: :model do
+RSpec.describe Packages::Tag, type: :model, feature_category: :package_registry do
let!(:project) { create(:project) }
let!(:package) { create(:npm_package, version: '1.0.2', project: project, updated_at: 3.days.ago) }
@@ -16,14 +16,14 @@ RSpec.describe Packages::Tag, type: :model do
it { is_expected.to validate_presence_of(:name) }
end
- describe '.for_packages' do
+ describe '.for_package_ids' do
let(:package2) { create(:package, project: project, updated_at: 2.days.ago) }
let(:package3) { create(:package, project: project, updated_at: 1.day.ago) }
let!(:tag1) { create(:packages_tag, package: package) }
let!(:tag2) { create(:packages_tag, package: package2) }
let!(:tag3) { create(:packages_tag, package: package3) }
- subject { described_class.for_packages(project.packages) }
+ subject { described_class.for_package_ids(project.packages) }
it { is_expected.to match_array([tag1, tag2, tag3]) }
@@ -34,6 +34,12 @@ RSpec.describe Packages::Tag, type: :model do
it { is_expected.to match_array([tag2, tag3]) }
end
+
+ context 'with package ids' do
+ subject { described_class.for_package_ids(project.packages.select(:id)) }
+
+ it { is_expected.to match_array([tag1, tag2, tag3]) }
+ end
end
describe '.with_name' do
diff --git a/spec/models/personal_access_token_spec.rb b/spec/models/personal_access_token_spec.rb
index f65b5ff824b..2320ff669d0 100644
--- a/spec/models/personal_access_token_spec.rb
+++ b/spec/models/personal_access_token_spec.rb
@@ -216,6 +216,18 @@ RSpec.describe PersonalAccessToken, feature_category: :authentication_and_author
expect(personal_access_token).to be_valid
end
+ context 'with feature flag disabled' do
+ before do
+ stub_feature_flags(admin_mode_for_api: false)
+ end
+
+ it "allows creating a token with `admin_mode` scope" do
+ personal_access_token.scopes = [:api, :admin_mode]
+
+ expect(personal_access_token).to be_valid
+ end
+ end
+
context 'when registry is disabled' do
before do
stub_container_registry_config(enabled: false)
@@ -353,19 +365,43 @@ RSpec.describe PersonalAccessToken, feature_category: :authentication_and_author
describe '`admin_mode scope' do
subject { create(:personal_access_token, user: user, scopes: ['api']) }
- context 'with administrator user' do
- let_it_be(:user) { create(:user, :admin) }
+ context 'with feature flag enabled' do
+ context 'with administrator user' do
+ let_it_be(:user) { create(:user, :admin) }
- it 'adds `admin_mode` scope before created' do
- expect(subject.scopes).to contain_exactly('api', 'admin_mode')
+ it 'does not add `admin_mode` scope before created' do
+ expect(subject.scopes).to contain_exactly('api')
+ end
+ end
+
+ context 'with normal user' do
+ let_it_be(:user) { create(:user) }
+
+ it 'does not add `admin_mode` scope before created' do
+ expect(subject.scopes).to contain_exactly('api')
+ end
end
end
- context 'with normal user' do
- let_it_be(:user) { create(:user) }
+ context 'with feature flag disabled' do
+ before do
+ stub_feature_flags(admin_mode_for_api: false)
+ end
+
+ context 'with administrator user' do
+ let_it_be(:user) { create(:user, :admin) }
- it 'does not add `admin_mode` scope before created' do
- expect(subject.scopes).to contain_exactly('api')
+ it 'adds `admin_mode` scope before created' do
+ expect(subject.scopes).to contain_exactly('api', 'admin_mode')
+ end
+ end
+
+ context 'with normal user' do
+ let_it_be(:user) { create(:user) }
+
+ it 'does not add `admin_mode` scope before created' do
+ expect(subject.scopes).to contain_exactly('api')
+ end
end
end
end
diff --git a/spec/models/preloaders/user_max_access_level_in_groups_preloader_spec.rb b/spec/models/preloaders/user_max_access_level_in_groups_preloader_spec.rb
index 5e2aaa8b456..7d04817b621 100644
--- a/spec/models/preloaders/user_max_access_level_in_groups_preloader_spec.rb
+++ b/spec/models/preloaders/user_max_access_level_in_groups_preloader_spec.rb
@@ -55,6 +55,43 @@ RSpec.describe Preloaders::UserMaxAccessLevelInGroupsPreloader do
let(:expected_query_count) { 0 }
end
+
+ context 'for groups arising from group shares' do
+ let_it_be(:group4) { create(:group, :private) }
+ let_it_be(:group4_subgroup) { create(:group, :private, parent: group4) }
+
+ let(:groups) { [group4, group4_subgroup] }
+
+ before do
+ create(:group_group_link, :guest, shared_with_group: group1, shared_group: group4)
+ end
+
+ context 'when `include_memberships_from_group_shares_in_preloader` feature flag is disabled' do
+ before do
+ stub_feature_flags(include_memberships_from_group_shares_in_preloader: false)
+ end
+
+ it 'sets access_level to `NO_ACCESS` in cache for groups arising from group shares' do
+ described_class.new(groups, user).execute
+
+ groups.each do |group|
+ cached_access_level = group.max_member_access_for_user(user)
+
+ expect(cached_access_level).to eq(Gitlab::Access::NO_ACCESS)
+ end
+ end
+ end
+
+ it 'sets the right access level in cache for groups arising from group shares' do
+ described_class.new(groups, user).execute
+
+ groups.each do |group|
+ cached_access_level = group.max_member_access_for_user(user)
+
+ expect(cached_access_level).to eq(Gitlab::Access::GUEST)
+ end
+ end
+ end
end
end
end
diff --git a/spec/models/preloaders/user_max_access_level_in_projects_preloader_spec.rb b/spec/models/preloaders/user_max_access_level_in_projects_preloader_spec.rb
index 1cfeeac49cd..de10653d87e 100644
--- a/spec/models/preloaders/user_max_access_level_in_projects_preloader_spec.rb
+++ b/spec/models/preloaders/user_max_access_level_in_projects_preloader_spec.rb
@@ -28,7 +28,7 @@ RSpec.describe Preloaders::UserMaxAccessLevelInProjectsPreloader do
end
end
- shared_examples '#execute' do
+ describe '#execute', :request_store do
let(:projects_arg) { projects }
context 'when user is present' do
@@ -70,16 +70,4 @@ RSpec.describe Preloaders::UserMaxAccessLevelInProjectsPreloader do
end
end
end
-
- describe '#execute', :request_store do
- include_examples '#execute'
-
- context 'when projects_preloader_fix is disabled' do
- before do
- stub_feature_flags(projects_preloader_fix: false)
- end
-
- include_examples '#execute'
- end
- end
end
diff --git a/spec/models/project_authorization_spec.rb b/spec/models/project_authorization_spec.rb
index df89e97a41f..dc4922d8114 100644
--- a/spec/models/project_authorization_spec.rb
+++ b/spec/models/project_authorization_spec.rb
@@ -94,11 +94,13 @@ RSpec.describe ProjectAuthorization do
end
end
- shared_examples_for 'logs the detail' do
+ shared_examples_for 'logs the detail' do |batch_size:|
it 'logs the detail' do
expect(Gitlab::AppLogger).to receive(:info).with(
entire_size: 3,
- message: 'Project authorizations refresh performed with delay'
+ message: 'Project authorizations refresh performed with delay',
+ total_delay: (3 / batch_size.to_f).ceil * ProjectAuthorization::SLEEP_DELAY,
+ **Gitlab::ApplicationContext.current
)
execute
@@ -124,7 +126,6 @@ RSpec.describe ProjectAuthorization do
before do
# Configure as if a replica database is enabled
allow(::Gitlab::Database::LoadBalancing).to receive(:primary_only?).and_return(false)
- stub_feature_flags(enable_minor_delay_during_project_authorizations_refresh: true)
end
shared_examples_for 'inserts the rows in batches, as per the `per_batch` size, without a delay between each batch' do
@@ -149,7 +150,7 @@ RSpec.describe ProjectAuthorization do
expect(user.project_authorizations.pluck(:user_id, :project_id, :access_level)).to match_array(attributes.map(&:values))
end
- it_behaves_like 'logs the detail'
+ it_behaves_like 'logs the detail', batch_size: 2
context 'when the GitLab installation does not have a replica database configured' do
before do
@@ -190,7 +191,6 @@ RSpec.describe ProjectAuthorization do
before do
# Configure as if a replica database is enabled
allow(::Gitlab::Database::LoadBalancing).to receive(:primary_only?).and_return(false)
- stub_feature_flags(enable_minor_delay_during_project_authorizations_refresh: true)
end
before_all do
@@ -221,7 +221,7 @@ RSpec.describe ProjectAuthorization do
expect(project.project_authorizations.pluck(:user_id)).not_to include(*user_ids)
end
- it_behaves_like 'logs the detail'
+ it_behaves_like 'logs the detail', batch_size: 2
context 'when the GitLab installation does not have a replica database configured' do
before do
@@ -262,7 +262,6 @@ RSpec.describe ProjectAuthorization do
before do
# Configure as if a replica database is enabled
allow(::Gitlab::Database::LoadBalancing).to receive(:primary_only?).and_return(false)
- stub_feature_flags(enable_minor_delay_during_project_authorizations_refresh: true)
end
before_all do
@@ -293,7 +292,7 @@ RSpec.describe ProjectAuthorization do
expect(user.project_authorizations.pluck(:project_id)).not_to include(*project_ids)
end
- it_behaves_like 'logs the detail'
+ it_behaves_like 'logs the detail', batch_size: 2
context 'when the GitLab installation does not have a replica database configured' do
before do
diff --git a/spec/models/project_ci_cd_setting_spec.rb b/spec/models/project_ci_cd_setting_spec.rb
index 5a32e103e0f..2c490c33747 100644
--- a/spec/models/project_ci_cd_setting_spec.rb
+++ b/spec/models/project_ci_cd_setting_spec.rb
@@ -27,6 +27,24 @@ RSpec.describe ProjectCiCdSetting do
end
end
+ describe '#set_default_for_inbound_job_token_scope_enabled' do
+ context 'when feature flag ci_inbound_job_token_scope is enabled' do
+ before do
+ stub_feature_flags(ci_inbound_job_token_scope: true)
+ end
+
+ it { is_expected.to be_inbound_job_token_scope_enabled }
+ end
+
+ context 'when feature flag ci_inbound_job_token_scope is disabled' do
+ before do
+ stub_feature_flags(ci_inbound_job_token_scope: false)
+ end
+
+ it { is_expected.not_to be_inbound_job_token_scope_enabled }
+ end
+ end
+
describe '#default_git_depth' do
let(:default_value) { described_class::DEFAULT_GIT_DEPTH }
diff --git a/spec/models/project_feature_spec.rb b/spec/models/project_feature_spec.rb
index fb6aaffdf22..fe0b46c3117 100644
--- a/spec/models/project_feature_spec.rb
+++ b/spec/models/project_feature_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ProjectFeature do
+RSpec.describe ProjectFeature, feature_category: :projects do
using RSpec::Parameterized::TableSyntax
let_it_be_with_reload(:project) { create(:project) }
@@ -10,6 +10,28 @@ RSpec.describe ProjectFeature do
it { is_expected.to belong_to(:project) }
+ describe 'default values' do
+ subject { Project.new.project_feature }
+
+ specify { expect(subject.builds_access_level).to eq(ProjectFeature::ENABLED) }
+ specify { expect(subject.issues_access_level).to eq(ProjectFeature::ENABLED) }
+ specify { expect(subject.forking_access_level).to eq(ProjectFeature::ENABLED) }
+ specify { expect(subject.merge_requests_access_level).to eq(ProjectFeature::ENABLED) }
+ specify { expect(subject.snippets_access_level).to eq(ProjectFeature::ENABLED) }
+ specify { expect(subject.wiki_access_level).to eq(ProjectFeature::ENABLED) }
+ specify { expect(subject.repository_access_level).to eq(ProjectFeature::ENABLED) }
+ specify { expect(subject.metrics_dashboard_access_level).to eq(ProjectFeature::PRIVATE) }
+ specify { expect(subject.operations_access_level).to eq(ProjectFeature::ENABLED) }
+ specify { expect(subject.security_and_compliance_access_level).to eq(ProjectFeature::PRIVATE) }
+ specify { expect(subject.monitor_access_level).to eq(ProjectFeature::ENABLED) }
+ specify { expect(subject.infrastructure_access_level).to eq(ProjectFeature::ENABLED) }
+ specify { expect(subject.feature_flags_access_level).to eq(ProjectFeature::ENABLED) }
+ specify { expect(subject.environments_access_level).to eq(ProjectFeature::ENABLED) }
+ specify { expect(subject.releases_access_level).to eq(ProjectFeature::ENABLED) }
+ specify { expect(subject.package_registry_access_level).to eq(ProjectFeature::ENABLED) }
+ specify { expect(subject.container_registry_access_level).to eq(ProjectFeature::ENABLED) }
+ end
+
describe 'PRIVATE_FEATURES_MIN_ACCESS_LEVEL_FOR_PRIVATE_PROJECT' do
it 'has higher level than that of PRIVATE_FEATURES_MIN_ACCESS_LEVEL' do
described_class::PRIVATE_FEATURES_MIN_ACCESS_LEVEL_FOR_PRIVATE_PROJECT.each do |feature, level|
diff --git a/spec/models/project_import_state_spec.rb b/spec/models/project_import_state_spec.rb
index e5232026c39..7ceb4931c4f 100644
--- a/spec/models/project_import_state_spec.rb
+++ b/spec/models/project_import_state_spec.rb
@@ -14,6 +14,30 @@ RSpec.describe ProjectImportState, type: :model, feature_category: :importers do
describe 'validations' do
it { is_expected.to validate_presence_of(:project) }
+
+ describe 'checksums attribute' do
+ let(:import_state) { build(:import_state, checksums: checksums) }
+
+ before do
+ import_state.validate
+ end
+
+ context 'when the checksums attribute has invalid fields' do
+ let(:checksums) { { fetched: { issue: :foo, note: 20 } } }
+
+ it 'adds errors' do
+ expect(import_state.errors.details.keys).to include(:checksums)
+ end
+ end
+
+ context 'when the checksums attribute has valid fields' do
+ let(:checksums) { { fetched: { issue: 8, note: 2 }, imported: { issue: 3, note: 2 } } }
+
+ it 'does not add errors' do
+ expect(import_state.errors.details.keys).not_to include(:checksums)
+ end
+ end
+ end
end
describe 'Project import job' do
@@ -199,6 +223,37 @@ RSpec.describe ProjectImportState, type: :model, feature_category: :importers do
.from(import_data).to(nil)
end
end
+
+ context 'state transition: started: [:finished, :canceled, :failed]' do
+ using RSpec::Parameterized::TableSyntax
+
+ let_it_be_with_reload(:project) { create(:project) }
+
+ where(
+ :import_type,
+ :import_status,
+ :transition,
+ :expected_checksums
+ ) do
+ 'github' | :started | :finish | { 'fetched' => {}, 'imported' => {} }
+ 'github' | :started | :cancel | { 'fetched' => {}, 'imported' => {} }
+ 'github' | :started | :fail_op | { 'fetched' => {}, 'imported' => {} }
+ 'github' | :scheduled | :cancel | {}
+ 'gitlab_project' | :started | :cancel | {}
+ end
+
+ with_them do
+ before do
+ create(:import_state, status: import_status, import_type: import_type, project: project)
+ end
+
+ it 'updates (or does not update) checksums' do
+ project.import_state.send(transition)
+
+ expect(project.import_state.checksums).to eq(expected_checksums)
+ end
+ end
+ end
end
describe 'clearing `jid` after finish', :clean_gitlab_redis_cache do
diff --git a/spec/models/project_setting_spec.rb b/spec/models/project_setting_spec.rb
index 94a2e2fe3f9..feb5985818b 100644
--- a/spec/models/project_setting_spec.rb
+++ b/spec/models/project_setting_spec.rb
@@ -96,6 +96,56 @@ RSpec.describe ProjectSetting, type: :model do
end
end
+ describe '#emails_enabled?' do
+ context "when a project does not have a parent group" do
+ let(:project_settings) { create(:project_setting, emails_enabled: true) }
+ let(:project) { create(:project, project_setting: project_settings) }
+
+ it "returns true" do
+ expect(project.emails_enabled?).to be_truthy
+ end
+
+ it "returns false when updating project settings" do
+ project.update_attribute(:emails_disabled, false)
+ expect(project.emails_enabled?).to be_truthy
+ end
+ end
+
+ context "when a project has a parent group" do
+ let(:namespace_settings) { create(:namespace_settings, emails_enabled: true) }
+ let(:project_settings) { create(:project_setting, emails_enabled: true) }
+ let(:group) { create(:group, namespace_settings: namespace_settings) }
+ let(:project) do
+ create(:project, namespace_id: group.id,
+ project_setting: project_settings)
+ end
+
+ context 'when emails have been disabled in parent group' do
+ it 'returns false' do
+ group.update_attribute(:emails_disabled, true)
+
+ expect(project.emails_enabled?).to be_falsey
+ end
+ end
+
+ context 'when emails are enabled in parent group' do
+ before do
+ allow(project.namespace).to receive(:emails_enabled?).and_return(true)
+ end
+
+ it 'returns true' do
+ expect(project.emails_enabled?).to be_truthy
+ end
+
+ it 'returns false when disabled at the project' do
+ project.update_attribute(:emails_disabled, true)
+
+ expect(project.emails_enabled?).to be_falsey
+ end
+ end
+ end
+ end
+
context 'when a parent group has a parent group' do
let(:namespace_settings) { create(:namespace_settings, show_diff_preview_in_email: false) }
let(:project_settings) { create(:project_setting, show_diff_preview_in_email: true) }
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 4ed85844a53..dfc8919e19d 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -112,6 +112,7 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
it { is_expected.to have_many(:uploads) }
it { is_expected.to have_many(:pipeline_schedules) }
it { is_expected.to have_many(:members_and_requesters) }
+ it { is_expected.to have_many(:namespace_members_and_requesters) }
it { is_expected.to have_many(:clusters) }
it { is_expected.to have_many(:management_clusters).class_name('Clusters::Cluster') }
it { is_expected.to have_many(:kubernetes_namespaces) }
@@ -121,8 +122,6 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects 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).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) }
@@ -404,6 +403,34 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
end
end
+ describe '#namespace_members_and_requesters' do
+ let_it_be(:project) { create(:project, :public) }
+ let_it_be(:requester) { create(:user) }
+ let_it_be(:developer) { create(:user) }
+ let_it_be(:invited_member) { create(:project_member, :invited, :owner, project: project) }
+
+ before_all do
+ project.request_access(requester)
+ project.add_developer(developer)
+ end
+
+ it 'includes the correct users' do
+ expect(project.namespace_members_and_requesters).to include(
+ Member.find_by(user: requester),
+ Member.find_by(user: developer),
+ Member.find(invited_member.id)
+ )
+ end
+
+ it 'is equivalent to #project_members' do
+ expect(project.namespace_members_and_requesters).to match_array(project.members_and_requesters)
+ end
+
+ it_behaves_like 'query without source filters' do
+ subject { project.namespace_members_and_requesters }
+ end
+ end
+
shared_examples 'polymorphic membership relationship' do
it do
expect(membership.attributes).to include(
@@ -452,6 +479,25 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
it_behaves_like 'member_namespace membership relationship'
end
+ describe '#namespace_members_and_requesters setters' do
+ let_it_be(:requested_at) { Time.current }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:membership) do
+ project.namespace_members_and_requesters.create!(
+ user: user, requested_at: requested_at, access_level: Gitlab::Access::DEVELOPER
+ )
+ end
+
+ it { expect(membership).to be_instance_of(ProjectMember) }
+ it { expect(membership.user).to eq user }
+ it { expect(membership.project).to eq project }
+ it { expect(membership.requested_at).to eq requested_at }
+
+ it_behaves_like 'polymorphic membership relationship'
+ it_behaves_like 'member_namespace membership relationship'
+ end
+
describe '#members & #requesters' do
let_it_be(:project) { create(:project, :public) }
let_it_be(:requester) { create(:user) }
@@ -920,6 +966,29 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
end
end
+ describe '#invalidate_personal_projects_count_of_owner' do
+ context 'for personal projects' do
+ let_it_be(:namespace_user) { create(:user) }
+ let_it_be(:project) { create(:project, namespace: namespace_user.namespace) }
+
+ it 'invalidates personal_project_count cache of the the owner of the personal namespace' do
+ expect(Rails.cache).to receive(:delete).with(['users', namespace_user.id, 'personal_projects_count'])
+
+ project.invalidate_personal_projects_count_of_owner
+ end
+ end
+
+ context 'for projects in groups' do
+ let_it_be(:project) { create(:project, namespace: create(:group)) }
+
+ it 'does not invalidates any cache' do
+ expect(Rails.cache).not_to receive(:delete)
+
+ project.invalidate_personal_projects_count_of_owner
+ end
+ end
+ end
+
describe '#default_pipeline_lock' do
let(:project) { build_stubbed(:project) }
@@ -2320,7 +2389,7 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
context 'shared runners' do
let(:project) { create(:project, shared_runners_enabled: shared_runners_enabled) }
- let(:specific_runner) { create(:ci_runner, :project, :online, projects: [project]) }
+ let(:project_runner) { create(:ci_runner, :project, :online, projects: [project]) }
let(:shared_runner) { create(:ci_runner, :instance, :online) }
let(:offline_runner) { create(:ci_runner, :instance) }
@@ -2331,8 +2400,8 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
is_expected.to be_falsey
end
- it 'has a specific runner' do
- specific_runner
+ it 'has a project runner' do
+ project_runner
is_expected.to be_truthy
end
@@ -2343,14 +2412,14 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
is_expected.to be_falsey
end
- it 'checks the presence of specific runner' do
- specific_runner
+ it 'checks the presence of project runner' do
+ project_runner
- expect(project.any_online_runners? { |runner| runner == specific_runner }).to be_truthy
+ expect(project.any_online_runners? { |runner| runner == project_runner }).to be_truthy
end
it 'returns false if match cannot be found' do
- specific_runner
+ project_runner
expect(project.any_online_runners? { false }).to be_falsey
end
@@ -3418,6 +3487,31 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
end
end
+ describe '#import_checksums' do
+ context 'with import_checksums' do
+ it 'returns the right checksums' do
+ project = create(:project)
+ create(:import_state, project: project, checksums: {
+ 'fetched' => {},
+ 'imported' => {}
+ })
+
+ expect(project.import_checksums).to eq(
+ 'fetched' => {},
+ 'imported' => {}
+ )
+ end
+ end
+
+ context 'without import_state' do
+ it 'returns empty hash' do
+ project = create(:project)
+
+ expect(project.import_checksums).to eq({})
+ end
+ end
+ end
+
describe '#jira_import_status' do
let_it_be(:project) { create(:project, import_type: 'jira') }
@@ -3852,10 +3946,21 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
end
describe '#emails_enabled?' do
- let(:project) { build(:project, emails_disabled: false) }
+ context 'without a persisted project_setting object' do
+ let(:project) { build(:project, emails_disabled: false) }
- it "is the opposite of emails_disabled" do
- expect(project.emails_enabled?).to be_truthy
+ it "is the opposite of emails_disabled" do
+ expect(project.emails_enabled?).to be_truthy
+ end
+ end
+
+ context 'with a persisted project_setting object' do
+ let(:project_settings) { create(:project_setting, emails_enabled: true) }
+ let(:project) { build(:project, emails_disabled: false, project_setting: project_settings) }
+
+ it "is the opposite of emails_disabled" do
+ expect(project.emails_enabled?).to be_truthy
+ end
end
end
@@ -4693,7 +4798,9 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
end
context 'with deploy token users' do
- let_it_be(:private_project) { create(:project, :private) }
+ let_it_be(:private_project) { create(:project, :private, description: 'Match') }
+ let_it_be(:private_project2) { create(:project, :private, description: 'Match') }
+ let_it_be(:private_project3) { create(:project, :private, description: 'Mismatch') }
subject { described_class.all.public_or_visible_to_user(user) }
@@ -4703,10 +4810,16 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
it { is_expected.to eq [] }
end
- context 'deploy token user with project' do
- let_it_be(:user) { create(:deploy_token, projects: [private_project]) }
+ context 'deploy token user with projects' do
+ let_it_be(:user) { create(:deploy_token, projects: [private_project, private_project2, private_project3]) }
+
+ it { is_expected.to contain_exactly(private_project, private_project2, private_project3) }
+
+ context 'with chained filter' do
+ subject { described_class.where(description: 'Match').public_or_visible_to_user(user) }
- it { is_expected.to include(private_project) }
+ it { is_expected.to contain_exactly(private_project, private_project2) }
+ end
end
end
end
@@ -5933,6 +6046,18 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
project.execute_hooks(data, :push_hooks)
end
+ it 'executes hooks which were backed off and are no longer backed off' do
+ project = create(:project)
+ hook = create(:project_hook, project: project, push_events: true)
+ WebHook::FAILURE_THRESHOLD.succ.times { hook.backoff! }
+
+ expect_any_instance_of(ProjectHook).to receive(:async_execute).once
+
+ travel_to(hook.disabled_until + 1.second) do
+ project.execute_hooks(data, :push_hooks)
+ end
+ end
+
it 'executes the system hooks with the specified scope' do
expect_any_instance_of(SystemHooksService).to receive(:execute_hooks).with(data, :merge_request_hooks)
@@ -7225,6 +7350,54 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
end
end
+ describe '#group_protected_branches' do
+ subject { project.group_protected_branches }
+
+ let(:project) { create(:project, group: group) }
+ let(:group) { create(:group) }
+ let(:protected_branch) { create(:protected_branch, group: group, project: nil) }
+
+ it 'returns protected branches of the group' do
+ is_expected.to match_array([protected_branch])
+ end
+
+ context 'when project belongs to namespace' do
+ let(:project) { create(:project) }
+
+ it 'returns empty relation' do
+ is_expected.to be_empty
+ end
+ end
+ end
+
+ describe '#all_protected_branches' do
+ let(:group) { create(:group) }
+ let!(:group_protected_branch) { create(:protected_branch, group: group, project: nil) }
+ let!(:project_protected_branch) { create(:protected_branch, project: subject) }
+
+ subject { create(:project, group: group) }
+
+ context 'when feature flag `group_protected_branches` enabled' do
+ before do
+ stub_feature_flags(group_protected_branches: true)
+ end
+
+ it 'return all protected branches' do
+ expect(subject.all_protected_branches).to match_array([group_protected_branch, project_protected_branch])
+ end
+ end
+
+ context 'when feature flag `group_protected_branches` disabled' do
+ before do
+ stub_feature_flags(group_protected_branches: false)
+ end
+
+ it 'return only project-level protected branches' do
+ expect(subject.all_protected_branches).to match_array([project_protected_branch])
+ end
+ end
+ end
+
describe '#lfs_objects_oids' do
let(:project) { create(:project) }
let(:lfs_object) { create(:lfs_object) }
@@ -8366,16 +8539,6 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
end
end
- describe '#work_items_create_from_markdown_feature_flag_enabled?' do
- let_it_be(:group_project) { create(:project, :in_subgroup) }
-
- it_behaves_like 'checks parent group feature flag' do
- let(:feature_flag_method) { :work_items_create_from_markdown_feature_flag_enabled? }
- let(:feature_flag) { :work_items_create_from_markdown }
- let(:subject_project) { group_project }
- end
- end
-
describe 'serialization' do
let(:object) { build(:project) }
@@ -8405,14 +8568,6 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
end
end
- context 'when feature flag is disabled' do
- before do
- stub_feature_flags(record_projects_target_platforms: false)
- end
-
- it_behaves_like 'does not enqueue a Projects::RecordTargetPlatformsWorker'
- end
-
context 'when not in gitlab.com' do
let(:com) { false }
@@ -8669,6 +8824,24 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do
end
end
+ describe '.is_importing' do
+ it 'returns projects that have import in progress' do
+ project_1 = create(:project, :import_scheduled, import_type: 'github')
+ project_2 = create(:project, :import_started, import_type: 'github')
+ create(:project, :import_finished, import_type: 'github')
+
+ expect(described_class.is_importing).to match_array([project_1, project_2])
+ end
+ end
+
+ it_behaves_like 'something that has web-hooks' do
+ let_it_be_with_reload(:object) { create(:project) }
+
+ def create_hook
+ create(:project_hook, project: object)
+ end
+ end
+
private
def finish_job(export_job)
diff --git a/spec/models/project_team_spec.rb b/spec/models/project_team_spec.rb
index 1fab07c1452..f4cf3130aa9 100644
--- a/spec/models/project_team_spec.rb
+++ b/spec/models/project_team_spec.rb
@@ -2,7 +2,7 @@
require "spec_helper"
-RSpec.describe ProjectTeam do
+RSpec.describe ProjectTeam, feature_category: :subgroups do
include ProjectForksHelper
let(:maintainer) { create(:user) }
diff --git a/spec/models/projects/data_transfer_spec.rb b/spec/models/projects/data_transfer_spec.rb
new file mode 100644
index 00000000000..6d3ddbdd74e
--- /dev/null
+++ b/spec/models/projects/data_transfer_spec.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Projects::DataTransfer, feature_category: :source_code_management do
+ let_it_be(:project) { create(:project) }
+
+ it { expect(subject).to be_valid }
+
+ describe 'associations' do
+ it { is_expected.to belong_to(:project) }
+ it { is_expected.to belong_to(:namespace) }
+ end
+
+ describe 'scopes' do
+ describe '.current_month' do
+ subject { described_class.current_month }
+
+ it 'returns data transfer for the current month' do
+ travel_to(Time.utc(2022, 5, 2)) do
+ _past_month = create(:project_data_transfer, project: project, date: '2022-04-01')
+ current_month = create(:project_data_transfer, project: project, date: '2022-05-01')
+
+ is_expected.to match_array([current_month])
+ end
+ end
+ end
+ end
+
+ describe '.beginning_of_month' do
+ subject { described_class.beginning_of_month(time) }
+
+ let(:time) { Time.utc(2022, 5, 2) }
+
+ it { is_expected.to eq(Time.utc(2022, 5, 1)) }
+ end
+
+ describe 'unique index' do
+ before do
+ create(:project_data_transfer, project: project, date: '2022-05-01')
+ end
+
+ it 'raises unique index violation' do
+ expect { create(:project_data_transfer, project: project, namespace: project.root_namespace, date: '2022-05-01') }
+ .to raise_error(ActiveRecord::RecordNotUnique)
+ end
+
+ context 'when project was moved from one namespace to another' do
+ it 'creates a new record' do
+ expect { create(:project_data_transfer, project: project, namespace: create(:namespace), date: '2022-05-01') }
+ .to change { described_class.count }.by(1)
+ end
+ end
+
+ context 'when a different project is created' do
+ it 'creates a new record' do
+ expect { create(:project_data_transfer, project: build(:project), date: '2022-05-01') }
+ .to change { described_class.count }.by(1)
+ end
+ end
+ end
+end
diff --git a/spec/models/protected_branch_spec.rb b/spec/models/protected_branch_spec.rb
index b623d534f29..71e22f848cc 100644
--- a/spec/models/protected_branch_spec.rb
+++ b/spec/models/protected_branch_spec.rb
@@ -214,7 +214,6 @@ RSpec.describe ProtectedBranch do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:protected_branch) { create(:protected_branch, project: project, name: "“jawn”") }
- let(:use_new_cache_implementation) { true }
let(:rely_on_new_cache) { true }
shared_examples_for 'hash based cache implementation' do
@@ -230,7 +229,6 @@ RSpec.describe ProtectedBranch do
end
before do
- stub_feature_flags(hash_based_cache_for_protected_branches: use_new_cache_implementation)
stub_feature_flags(rely_on_protected_branches_cache: rely_on_new_cache)
allow(described_class).to receive(:matching).and_call_original
@@ -296,48 +294,6 @@ RSpec.describe ProtectedBranch do
expect(described_class.protected?(project, protected_branch.name)).to eq(true)
end
end
-
- context 'when feature flag hash_based_cache_for_protected_branches is off' do
- let(:use_new_cache_implementation) { false }
-
- it 'does not call hash based cache implementation' do
- expect(ProtectedBranches::CacheService).not_to receive(:new)
- expect(Rails.cache).to receive(:fetch).and_call_original
-
- described_class.protected?(project, 'missing-branch')
- end
-
- it 'correctly invalidates a cache' do
- expect(described_class).to receive(:matching).with(protected_branch.name, protected_refs: anything).once.and_call_original
-
- create(:protected_branch, project: project, name: "bar")
- # the cache is invalidated because the project has been "updated"
- expect(described_class.protected?(project, protected_branch.name)).to eq(true)
- end
-
- it 'sets expires_in of 1 hour for the Rails cache key' do
- cache_key = described_class.protected_ref_cache_key(project, protected_branch.name)
-
- expect(Rails.cache).to receive(:fetch).with(cache_key, expires_in: 1.hour)
-
- described_class.protected?(project, protected_branch.name)
- end
-
- context 'when project is updated' do
- it 'invalidates Rails cache' do
- expect(described_class).to receive(:matching).with(protected_branch.name, protected_refs: anything).once.and_call_original
-
- project.touch
-
- described_class.protected?(project, protected_branch.name)
- end
- end
-
- it 'correctly uses the cached version' do
- expect(described_class).not_to receive(:matching)
- expect(described_class.protected?(project, protected_branch.name)).to eq(true)
- end
- end
end
end
@@ -385,23 +341,61 @@ RSpec.describe ProtectedBranch do
end
describe "#allow_force_push?" do
- context "when the attr allow_force_push is true" do
- let(:subject_branch) { create(:protected_branch, allow_force_push: true, name: "foo") }
+ context "when feature flag disabled" do
+ before do
+ stub_feature_flags(group_protected_branches: false)
+ end
+
+ let(:subject_branch) { create(:protected_branch, allow_force_push: allow_force_push, name: "foo") }
+ let(:project) { subject_branch.project }
+
+ context "when the attr allow_force_push is true" do
+ let(:allow_force_push) { true }
- it "returns true" do
- project = subject_branch.project
+ it "returns true" do
+ expect(described_class.allow_force_push?(project, "foo")).to eq(true)
+ end
+ end
- expect(described_class.allow_force_push?(project, "foo")).to eq(true)
+ context "when the attr allow_force_push is false" do
+ let(:allow_force_push) { false }
+
+ it "returns false" do
+ expect(described_class.allow_force_push?(project, "foo")).to eq(false)
+ end
end
end
- context "when the attr allow_force_push is false" do
- let(:subject_branch) { create(:protected_branch, allow_force_push: false, name: "foo") }
+ context "when feature flag enabled" do
+ using RSpec::Parameterized::TableSyntax
+
+ let_it_be(:group) { create(:group) }
+ let_it_be(:project) { create(:project, group: group) }
- it "returns false" do
- project = subject_branch.project
+ where(:group_level_value, :project_level_value, :result) do
+ true | false | true
+ false | true | false
+ true | nil | true
+ false | nil | false
+ nil | nil | false
+ end
+
+ with_them do
+ before do
+ stub_feature_flags(group_protected_branches: true)
+
+ unless group_level_value.nil?
+ create(:protected_branch, allow_force_push: group_level_value, name: "foo", project: nil, group: group)
+ end
+
+ unless project_level_value.nil?
+ create(:protected_branch, allow_force_push: project_level_value, name: "foo", project: project)
+ end
+ end
- expect(described_class.allow_force_push?(project, "foo")).to eq(false)
+ it "returns result" do
+ expect(described_class.allow_force_push?(project, "foo")).to eq(result)
+ end
end
end
end
@@ -434,6 +428,36 @@ RSpec.describe ProtectedBranch do
end
end
+ describe '.protected_refs' do
+ let_it_be(:project) { create(:project) }
+
+ subject { described_class.protected_refs(project) }
+
+ context 'when feature flag enabled' do
+ before do
+ stub_feature_flags(group_protected_branches: true)
+ end
+
+ it 'call `all_protected_branches`' do
+ expect(project).to receive(:all_protected_branches)
+
+ subject
+ end
+ end
+
+ context 'when feature flag disabled' do
+ before do
+ stub_feature_flags(group_protected_branches: false)
+ end
+
+ it 'call `protected_branches`' do
+ expect(project).to receive(:protected_branches)
+
+ subject
+ end
+ end
+ end
+
describe '.by_name' do
let!(:protected_branch) { create(:protected_branch, name: 'master') }
let!(:another_protected_branch) { create(:protected_branch, name: 'stable') }
@@ -502,4 +526,22 @@ RSpec.describe ProtectedBranch do
it { is_expected.not_to be_default_branch }
end
end
+
+ describe '#group_level?' do
+ context 'when entity is a Group' do
+ before do
+ subject.assign_attributes(project: nil, group: build(:group))
+ end
+
+ it { is_expected.to be_group_level }
+ end
+
+ context 'when entity is a Project' do
+ before do
+ subject.assign_attributes(project: build(:project), group: nil)
+ end
+
+ it { is_expected.not_to be_group_level }
+ end
+ end
end
diff --git a/spec/models/protected_tag/create_access_level_spec.rb b/spec/models/protected_tag/create_access_level_spec.rb
new file mode 100644
index 00000000000..566f8695388
--- /dev/null
+++ b/spec/models/protected_tag/create_access_level_spec.rb
@@ -0,0 +1,144 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ProtectedTag::CreateAccessLevel, feature_category: :source_code_management do
+ describe 'associations' do
+ it { is_expected.to belong_to(:deploy_key) }
+ end
+
+ describe 'validations', :aggregate_failures do
+ let_it_be(:protected_tag) { create(:protected_tag) }
+
+ it 'verifies access levels' do
+ is_expected.to validate_inclusion_of(:access_level).in_array(
+ [
+ Gitlab::Access::MAINTAINER,
+ Gitlab::Access::DEVELOPER,
+ Gitlab::Access::NO_ACCESS
+ ]
+ )
+ end
+
+ context 'when deploy key enabled for the project' do
+ let(:deploy_key) { create(:deploy_key, projects: [protected_tag.project]) }
+
+ it 'is valid' do
+ level = build(:protected_tag_create_access_level, protected_tag: protected_tag, deploy_key: deploy_key)
+
+ expect(level).to be_valid
+ end
+ end
+
+ context 'when a record exists with the same access level' do
+ before do
+ create(:protected_tag_create_access_level, protected_tag: protected_tag)
+ end
+
+ it 'is not valid' do
+ level = build(:protected_tag_create_access_level, protected_tag: protected_tag)
+
+ expect(level).to be_invalid
+ expect(level.errors.full_messages).to include('Access level has already been taken')
+ end
+ end
+
+ context 'when a deploy key already added for this access level' do
+ let!(:create_access_level) do
+ create(:protected_tag_create_access_level, protected_tag: protected_tag, deploy_key: deploy_key)
+ end
+
+ let(:deploy_key) { create(:deploy_key, projects: [protected_tag.project]) }
+
+ it 'is not valid' do
+ level = build(:protected_tag_create_access_level, protected_tag: protected_tag, deploy_key: deploy_key)
+
+ expect(level).to be_invalid
+ expect(level.errors.full_messages).to contain_exactly('Deploy key has already been taken')
+ end
+ end
+
+ context 'when deploy key is not enabled for the project' do
+ let(:create_access_level) do
+ build(:protected_tag_create_access_level, protected_tag: protected_tag, deploy_key: create(:deploy_key))
+ end
+
+ it 'returns an error' do
+ expect(create_access_level).to be_invalid
+ expect(create_access_level.errors.full_messages).to contain_exactly(
+ 'Deploy key is not enabled for this project'
+ )
+ end
+ end
+ end
+
+ describe '#check_access' do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:protected_tag) { create(:protected_tag, :no_one_can_create, project: project) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:deploy_key) { create(:deploy_key, user: user) }
+
+ let!(:deploy_keys_project) do
+ create(:deploy_keys_project, project: project, deploy_key: deploy_key, can_push: can_push)
+ end
+
+ let(:create_access_level) { protected_tag.create_access_levels.first }
+ let(:can_push) { true }
+
+ before_all do
+ project.add_maintainer(user)
+ end
+
+ it { expect(create_access_level.check_access(user)).to be_falsey }
+
+ context 'when this create_access_level is tied to a deploy key' do
+ let(:create_access_level) do
+ create(:protected_tag_create_access_level, protected_tag: protected_tag, deploy_key: deploy_key)
+ end
+
+ context 'when the deploy key is among the active keys for this project' do
+ it { expect(create_access_level.check_access(user)).to be_truthy }
+ end
+
+ context 'when user is missing' do
+ it { expect(create_access_level.check_access(nil)).to be_falsey }
+ end
+
+ context 'when deploy key does not belong to the user' do
+ let(:another_user) { create(:user) }
+
+ it { expect(create_access_level.check_access(another_user)).to be_falsey }
+ end
+
+ context 'when user cannot access the project' do
+ before do
+ allow(user).to receive(:can?).with(:read_project, project).and_return(false)
+ end
+
+ it { expect(create_access_level.check_access(user)).to be_falsey }
+ end
+
+ context 'when the deploy key is not among the active keys of this project' do
+ let(:can_push) { false }
+
+ it { expect(create_access_level.check_access(user)).to be_falsey }
+ end
+ end
+ end
+
+ describe '#type' do
+ let(:create_access_level) { build(:protected_tag_create_access_level) }
+
+ it 'returns :role by default' do
+ expect(create_access_level.type).to eq(:role)
+ end
+
+ context 'when a deploy key is tied to the protected branch' do
+ let(:create_access_level) { build(:protected_tag_create_access_level, deploy_key: build(:deploy_key)) }
+
+ it 'returns :deploy_key' do
+ expect(create_access_level.type).to eq(:deploy_key)
+ end
+ end
+ end
+end
diff --git a/spec/models/release_highlight_spec.rb b/spec/models/release_highlight_spec.rb
index 4148452f849..0391acc3781 100644
--- a/spec/models/release_highlight_spec.rb
+++ b/spec/models/release_highlight_spec.rb
@@ -6,7 +6,8 @@ RSpec.describe ReleaseHighlight, :clean_gitlab_redis_cache, feature_category: :r
let(:fixture_dir_glob) { Dir.glob(File.join(Rails.root, 'spec', 'fixtures', 'whats_new', '*.yml')).grep(/\d*_(\d*_\d*)\.yml$/) }
before do
- allow(Dir).to receive(:glob).with(Rails.root.join('data', 'whats_new', '*.yml')).and_return(fixture_dir_glob)
+ allow(Dir).to receive(:glob).and_call_original
+ allow(Dir).to receive(:glob).with(described_class.whats_new_path).and_return(fixture_dir_glob)
Gitlab::CurrentSettings.update!(whats_new_variant: ApplicationSetting.whats_new_variants[:all_tiers])
end
diff --git a/spec/models/release_spec.rb b/spec/models/release_spec.rb
index 5ed4eb7d233..880fb21b7af 100644
--- a/spec/models/release_spec.rb
+++ b/spec/models/release_spec.rb
@@ -71,25 +71,12 @@ RSpec.describe Release do
subject { build(:release, project: project, name: 'Release 1.0') }
it { is_expected.to validate_presence_of(:author_id) }
-
- context 'when feature flag is disabled' do
- before do
- stub_feature_flags(validate_release_with_author: false)
- end
-
- it { is_expected.not_to validate_presence_of(:author_id) }
- end
end
- # Mimic releases created before 11.7
- # See: https://gitlab.com/gitlab-org/gitlab/-/blob/8e5a110b01f842d8b6a702197928757a40ce9009/app/models/release.rb#L14
+ # Deleting user along with their contributions, nullifies releases author_id.
context 'when updating existing release without author' do
let(:release) { create(:release, :legacy) }
- before do
- stub_feature_flags(validate_release_with_author: false)
- end
-
it 'updates successfully' do
release.description += 'Update'
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index a3d2f9a09fb..b8780b3faae 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -566,16 +566,6 @@ RSpec.describe Repository, feature_category: :source_code_management do
expect(commit_ids).to include(*expected_commit_ids)
expect(commit_ids).not_to include('913c66a37b4a45b9769037c55c2d238bd0942d2e')
end
-
- context 'when feature flag "commit_search_trailing_spaces" is disabled' do
- before do
- stub_feature_flags(commit_search_trailing_spaces: false)
- end
-
- it 'returns an empty list' do
- expect(commit_ids).to be_empty
- end
- end
end
describe 'when storage is broken', :broken_storage do
@@ -1858,6 +1848,8 @@ RSpec.describe Repository, feature_category: :source_code_management do
end
describe '#expire_root_ref_cache' do
+ let(:project) { create(:project) }
+
it 'expires the root reference cache' do
repository.root_ref
@@ -1959,6 +1951,40 @@ RSpec.describe Repository, feature_category: :source_code_management do
end
end
+ describe '#merge_to_branch' do
+ let(:merge_request) do
+ create(:merge_request, source_branch: 'feature', target_branch: project.default_branch, source_project: project)
+ end
+
+ it 'merges two branches and returns the merge commit id' do
+ message = 'New merge commit'
+ merge_commit_id =
+ repository.merge_to_branch(user,
+ source_sha: merge_request.diff_head_sha,
+ target_branch: merge_request.target_branch,
+ target_sha: repository.commit(merge_request.target_branch).sha,
+ message: message)
+
+ expect(repository.commit(merge_commit_id).message).to eq(message)
+ expect(repository.commit(merge_request.target_branch).sha).to eq(merge_commit_id)
+ end
+
+ it 'does not merge if target branch has been changed' do
+ target_sha = project.commit.sha
+
+ repository.create_file(user, 'file.txt', 'CONTENT', message: 'Add file', branch_name: project.default_branch)
+
+ merge_commit_id =
+ repository.merge_to_branch(user,
+ source_sha: merge_request.diff_head_sha,
+ target_branch: merge_request.target_branch,
+ target_sha: target_sha,
+ message: 'New merge commit')
+
+ expect(merge_commit_id).to be_nil
+ end
+ end
+
describe '#merge_to_ref' do
let(:merge_request) do
create(:merge_request, source_branch: 'feature',
@@ -1985,15 +2011,20 @@ RSpec.describe Repository, feature_category: :source_code_management do
end
describe '#ff_merge' do
+ let(:target_branch) { 'ff-target' }
+ let(:merge_request) do
+ create(:merge_request, source_branch: 'feature', target_branch: target_branch, source_project: project)
+ end
+
before do
- repository.add_branch(user, 'ff-target', 'feature~5')
+ repository.add_branch(user, target_branch, 'feature~5')
end
it 'merges the code and return the commit id' do
- merge_request = create(:merge_request, source_branch: 'feature', target_branch: 'ff-target', source_project: project)
merge_commit_id = repository.ff_merge(user,
merge_request.diff_head_sha,
merge_request.target_branch,
+ target_sha: repository.commit(merge_request.target_branch).sha,
merge_request: merge_request)
merge_commit = repository.commit(merge_commit_id)
@@ -2002,14 +2033,24 @@ RSpec.describe Repository, feature_category: :source_code_management do
end
it 'sets the `in_progress_merge_commit_sha` flag for the given merge request' do
- merge_request = create(:merge_request, source_branch: 'feature', target_branch: 'ff-target', source_project: project)
merge_commit_id = repository.ff_merge(user,
merge_request.diff_head_sha,
merge_request.target_branch,
+ target_sha: repository.commit(merge_request.target_branch).sha,
merge_request: merge_request)
expect(merge_request.in_progress_merge_commit_sha).to eq(merge_commit_id)
end
+
+ it 'does not merge if target branch has been changed' do
+ target_sha = project.commit(target_branch).sha
+
+ repository.create_file(user, 'file.txt', 'CONTENT', message: 'Add file', branch_name: target_branch)
+
+ merge_commit_id = repository.ff_merge(user, merge_request.diff_head_sha, target_branch, target_sha: target_sha)
+
+ expect(merge_commit_id).to be_nil
+ end
end
describe '#rebase' do
@@ -2580,17 +2621,17 @@ RSpec.describe Repository, feature_category: :source_code_management do
it 'returns the first avatar file found in the repository' do
expect(repository).to receive(:file_on_head)
- .with(:avatar)
- .and_return(double(:tree, path: 'logo.png'))
+ .with(:avatar)
+ .and_return(double(:tree, path: 'logo.png'))
expect(repository.avatar).to eq('logo.png')
end
it 'caches the output' do
expect(repository).to receive(:file_on_head)
- .with(:avatar)
- .once
- .and_return(double(:tree, path: 'logo.png'))
+ .with(:avatar)
+ .once
+ .and_return(double(:tree, path: 'logo.png'))
2.times { expect(repository.avatar).to eq('logo.png') }
end
@@ -2718,26 +2759,12 @@ RSpec.describe Repository, feature_category: :source_code_management do
end
it 'caches the response' do
- expect(repository).to receive(:search_files_by_regexp).and_call_original.once
+ expect(repository.head_tree).to receive(:readme_path).and_call_original.once
2.times do
expect(repository.readme_path).to eq("README.md")
end
end
-
- context 'when "readme_from_gitaly" FF is disabled' do
- before do
- stub_feature_flags(readme_from_gitaly: false)
- end
-
- it 'caches the response' do
- expect(repository.head_tree).to receive(:readme_path).and_call_original.once
-
- 2.times do
- expect(repository.readme_path).to eq("README.md")
- end
- end
- end
end
end
end
@@ -2803,7 +2830,7 @@ RSpec.describe Repository, feature_category: :source_code_management do
context 'with a non-existing repository' do
it 'returns nil' do
- expect(repository).to receive(:head_commit).and_return(nil)
+ expect(repository).to receive(:root_ref).and_return(nil)
expect(repository.head_tree).to be_nil
end
@@ -2820,7 +2847,7 @@ RSpec.describe Repository, feature_category: :source_code_management do
context 'using a non-existing repository' do
before do
- allow(repository).to receive(:head_commit).and_return(nil)
+ allow(repository).to receive(:root_ref).and_return(nil)
end
it { is_expected.to be_nil }
diff --git a/spec/models/resource_event_spec.rb b/spec/models/resource_event_spec.rb
index f40c192ab2b..62bd5314b69 100644
--- a/spec/models/resource_event_spec.rb
+++ b/spec/models/resource_event_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ResourceEvent, feature_category: :team_planing, type: :model do
+RSpec.describe ResourceEvent, feature_category: :team_planning, type: :model do
let(:dummy_resource_label_event_class) do
Class.new(ResourceEvent) do
self.table_name = 'resource_label_events'
diff --git a/spec/models/resource_label_event_spec.rb b/spec/models/resource_label_event_spec.rb
index 87f3b9fb2bb..eb28010d57f 100644
--- a/spec/models/resource_label_event_spec.rb
+++ b/spec/models/resource_label_event_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ResourceLabelEvent, feature_category: :team_planing, type: :model do
+RSpec.describe ResourceLabelEvent, feature_category: :team_planning, type: :model do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:issue) { create(:issue, project: project) }
let_it_be(:merge_request) { create(:merge_request, source_project: project) }
diff --git a/spec/models/resource_milestone_event_spec.rb b/spec/models/resource_milestone_event_spec.rb
index 11b704ceadf..d237a16da8f 100644
--- a/spec/models/resource_milestone_event_spec.rb
+++ b/spec/models/resource_milestone_event_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ResourceMilestoneEvent, feature_category: :team_planing, type: :model do
+RSpec.describe ResourceMilestoneEvent, feature_category: :team_planning, type: :model do
it_behaves_like 'a resource event'
it_behaves_like 'a resource event for issues'
it_behaves_like 'a resource event for merge requests'
diff --git a/spec/models/resource_state_event_spec.rb b/spec/models/resource_state_event_spec.rb
index 04e4359a3ff..a6d6b507b69 100644
--- a/spec/models/resource_state_event_spec.rb
+++ b/spec/models/resource_state_event_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ResourceStateEvent, feature_category: :team_planing, type: :model do
+RSpec.describe ResourceStateEvent, feature_category: :team_planning, type: :model do
subject { build(:resource_state_event, issue: issue) }
let(:issue) { create(:issue) }
diff --git a/spec/models/service_desk_setting_spec.rb b/spec/models/service_desk_setting_spec.rb
index c1ec35732b8..32c36375a3d 100644
--- a/spec/models/service_desk_setting_spec.rb
+++ b/spec/models/service_desk_setting_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe ServiceDeskSetting do
+RSpec.describe ServiceDeskSetting, feature_category: :service_desk do
describe 'validations' do
subject(:service_desk_setting) { create(:service_desk_setting) }
@@ -12,6 +12,48 @@ RSpec.describe ServiceDeskSetting do
it { is_expected.to allow_value('abc123_').for(:project_key) }
it { is_expected.not_to allow_value('abc 12').for(:project_key).with_message("can contain only lowercase letters, digits, and '_'.") }
it { is_expected.not_to allow_value('Big val').for(:project_key) }
+ it { is_expected.to validate_length_of(:custom_email).is_at_most(255) }
+ it { is_expected.to validate_length_of(:custom_email_smtp_address).is_at_most(255) }
+ it { is_expected.to validate_length_of(:custom_email_smtp_username).is_at_most(255) }
+
+ describe '#custom_email_enabled' do
+ it { expect(subject.custom_email_enabled).to be_falsey }
+ it { expect(described_class.new(custom_email_enabled: true).custom_email_enabled).to be_truthy }
+ end
+
+ context 'when custom_email_enabled is true' do
+ before do
+ subject.custom_email_enabled = true
+ end
+
+ it { is_expected.to validate_presence_of(:custom_email) }
+ it { is_expected.to validate_uniqueness_of(:custom_email).allow_nil }
+ it { is_expected.to allow_value('support@example.com').for(:custom_email) }
+ it { is_expected.to allow_value('support@xn--brggen-4ya.de').for(:custom_email) } # converted domain name with umlaut
+ it { is_expected.to allow_value('support1@shop.example.com').for(:custom_email) }
+ it { is_expected.to allow_value('support-shop_with.crazy-address@shop.example.com').for(:custom_email) }
+ it { is_expected.not_to allow_value('support@example@example.com').for(:custom_email) }
+ it { is_expected.not_to allow_value('support.example.com').for(:custom_email) }
+ it { is_expected.not_to allow_value('example.com').for(:custom_email) }
+ it { is_expected.not_to allow_value('example').for(:custom_email) }
+ it { is_expected.not_to allow_value('" "@example.org').for(:custom_email) }
+ it { is_expected.not_to allow_value('support+12@example.com').for(:custom_email) }
+ it { is_expected.not_to allow_value('user@[IPv6:2001:db8::1]').for(:custom_email) }
+ it { is_expected.not_to allow_value('"><script>alert(1);</script>"@example.org').for(:custom_email) }
+ it { is_expected.not_to allow_value('file://example').for(:custom_email) }
+ it { is_expected.not_to allow_value('no email at all').for(:custom_email) }
+
+ it { is_expected.to validate_presence_of(:custom_email_smtp_username) }
+
+ it { is_expected.to validate_presence_of(:custom_email_smtp_port) }
+ it { is_expected.to validate_numericality_of(:custom_email_smtp_port).only_integer.is_greater_than(0) }
+
+ it { is_expected.to validate_presence_of(:custom_email_smtp_address) }
+ it { is_expected.to allow_value('smtp.gmail.com').for(:custom_email_smtp_address) }
+ it { is_expected.not_to allow_value('https://example.com').for(:custom_email_smtp_address) }
+ it { is_expected.not_to allow_value('file://example').for(:custom_email_smtp_address) }
+ it { is_expected.not_to allow_value('/example').for(:custom_email_smtp_address) }
+ end
describe '.valid_issue_template' do
let_it_be(:project) { create(:project, :custom_repo, files: { '.gitlab/issue_templates/service_desk.md' => 'template' }) }
@@ -67,6 +109,27 @@ RSpec.describe ServiceDeskSetting do
end
end
+ describe 'encrypted password' do
+ let_it_be(:settings) do
+ create(
+ :service_desk_setting,
+ custom_email_enabled: true,
+ custom_email: 'supersupport@example.com',
+ custom_email_smtp_address: 'smtp.example.com',
+ custom_email_smtp_port: 587,
+ custom_email_smtp_username: 'supersupport@example.com',
+ custom_email_smtp_password: 'supersecret'
+ )
+ end
+
+ it 'saves and retrieves the encrypted custom email smtp password and iv correctly' do
+ expect(settings.encrypted_custom_email_smtp_password).not_to be_nil
+ expect(settings.encrypted_custom_email_smtp_password_iv).not_to be_nil
+
+ expect(settings.custom_email_smtp_password).to eq('supersecret')
+ end
+ end
+
describe 'associations' do
it { is_expected.to belong_to(:project) }
end
diff --git a/spec/models/user_detail_spec.rb b/spec/models/user_detail_spec.rb
index 1893b6530a5..7d433896cf8 100644
--- a/spec/models/user_detail_spec.rb
+++ b/spec/models/user_detail_spec.rb
@@ -38,6 +38,27 @@ RSpec.describe UserDetail do
it { is_expected.to validate_length_of(:skype).is_at_most(500) }
end
+ describe '#discord' do
+ it { is_expected.to validate_length_of(:discord).is_at_most(500) }
+
+ context 'when discord is set' do
+ let_it_be(:user_detail) { create(:user_detail) }
+
+ it 'accepts a valid discord user id' do
+ user_detail.discord = '1234567890123456789'
+
+ expect(user_detail).to be_valid
+ end
+
+ it 'throws an error when other url format is wrong' do
+ user_detail.discord = '123456789'
+
+ expect(user_detail).not_to be_valid
+ expect(user_detail.errors.full_messages).to match_array([_('Discord must contain only a discord user ID.')])
+ end
+ end
+ end
+
describe '#location' do
it { is_expected.to validate_length_of(:location).is_at_most(500) }
end
@@ -72,11 +93,12 @@ RSpec.describe UserDetail do
let(:user_detail) do
create(:user_detail,
bio: 'bio',
+ discord: '1234567890123456789',
linkedin: 'linkedin',
- twitter: 'twitter',
- skype: 'skype',
location: 'location',
organization: 'organization',
+ skype: 'skype',
+ twitter: 'twitter',
website_url: 'https://example.com')
end
@@ -90,11 +112,12 @@ RSpec.describe UserDetail do
end
it_behaves_like 'prevents `nil` value', :bio
+ it_behaves_like 'prevents `nil` value', :discord
it_behaves_like 'prevents `nil` value', :linkedin
- it_behaves_like 'prevents `nil` value', :twitter
- it_behaves_like 'prevents `nil` value', :skype
it_behaves_like 'prevents `nil` value', :location
it_behaves_like 'prevents `nil` value', :organization
+ it_behaves_like 'prevents `nil` value', :skype
+ it_behaves_like 'prevents `nil` value', :twitter
it_behaves_like 'prevents `nil` value', :website_url
end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index e2e4e4248d8..e87667d9604 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe User, feature_category: :users do
+RSpec.describe User, feature_category: :user_profile do
include ProjectForksHelper
include TermsHelper
include ExclusiveLeaseHelpers
@@ -102,6 +102,9 @@ RSpec.describe User, feature_category: :users do
it { is_expected.to delegate_method(:requires_credit_card_verification).to(:user_detail).allow_nil }
it { is_expected.to delegate_method(:requires_credit_card_verification=).to(:user_detail).with_arguments(:args).allow_nil }
+ it { is_expected.to delegate_method(:discord).to(:user_detail).allow_nil }
+ it { is_expected.to delegate_method(:discord=).to(:user_detail).with_arguments(:args).allow_nil }
+
it { is_expected.to delegate_method(:linkedin).to(:user_detail).allow_nil }
it { is_expected.to delegate_method(:linkedin=).to(:user_detail).with_arguments(:args).allow_nil }
@@ -2126,6 +2129,7 @@ RSpec.describe User, feature_category: :users do
expect(user.encrypted_otp_secret_salt).to be_nil
expect(user.otp_backup_codes).to be_nil
expect(user.otp_grace_period_started_at).to be_nil
+ expect(user.otp_secret_expires_at).to be_nil
end
end
@@ -2401,21 +2405,6 @@ RSpec.describe User, feature_category: :users do
it_behaves_like 'manageable groups examples'
end
end
-
- describe '#manageable_groups_with_routes' do
- it 'eager loads routes from manageable groups' do
- control_count =
- ActiveRecord::QueryRecorder.new(skip_cached: false) do
- user.manageable_groups_with_routes.map(&:route)
- end.count
-
- create(:group, parent: subgroup)
-
- expect do
- user.manageable_groups_with_routes.map(&:route)
- end.not_to exceed_all_query_limit(control_count)
- end
- end
end
end
diff --git a/spec/models/wiki_directory_spec.rb b/spec/models/wiki_directory_spec.rb
index 44c6f6c9c1a..1b177934ace 100644
--- a/spec/models/wiki_directory_spec.rb
+++ b/spec/models/wiki_directory_spec.rb
@@ -13,15 +13,20 @@ RSpec.describe WikiDirectory do
let_it_be(:toplevel1) { build(:wiki_page, title: 'aaa-toplevel1') }
let_it_be(:toplevel2) { build(:wiki_page, title: 'zzz-toplevel2') }
let_it_be(:toplevel3) { build(:wiki_page, title: 'zzz-toplevel3') }
+ let_it_be(:parent1) { build(:wiki_page, title: 'parent1') }
+ let_it_be(:parent2) { build(:wiki_page, title: 'parent2') }
let_it_be(:child1) { build(:wiki_page, title: 'parent1/child1') }
let_it_be(:child2) { build(:wiki_page, title: 'parent1/child2') }
let_it_be(:child3) { build(:wiki_page, title: 'parent2/child3') }
+ let_it_be(:subparent) { build(:wiki_page, title: 'parent1/subparent') }
let_it_be(:grandchild1) { build(:wiki_page, title: 'parent1/subparent/grandchild1') }
let_it_be(:grandchild2) { build(:wiki_page, title: 'parent1/subparent/grandchild2') }
it 'returns a nested array of entries' do
entries = described_class.group_pages(
- [toplevel1, toplevel2, toplevel3, child1, child2, child3, grandchild1, grandchild2].sort_by(&:title)
+ [toplevel1, toplevel2, toplevel3,
+ parent1, parent2, child1, child2, child3,
+ subparent, grandchild1, grandchild2].sort_by(&:title)
)
expect(entries).to match(
@@ -95,7 +100,7 @@ RSpec.describe WikiDirectory do
describe '#to_partial_path' do
it 'returns the relative path to the partial to be used' do
- expect(directory.to_partial_path).to eq('../shared/wikis/wiki_directory')
+ expect(directory.to_partial_path).to eq('shared/wikis/wiki_directory')
end
end
end
diff --git a/spec/models/wiki_page_spec.rb b/spec/models/wiki_page_spec.rb
index fcb041aebe5..21da06a222f 100644
--- a/spec/models/wiki_page_spec.rb
+++ b/spec/models/wiki_page_spec.rb
@@ -912,7 +912,7 @@ RSpec.describe WikiPage do
describe '#to_partial_path' do
it 'returns the relative path to the partial to be used' do
- expect(build_wiki_page(container).to_partial_path).to eq('../shared/wikis/wiki_page')
+ expect(build_wiki_page(container).to_partial_path).to eq('shared/wikis/wiki_page')
end
end
diff --git a/spec/models/work_item_spec.rb b/spec/models/work_item_spec.rb
index 0bedcc9791f..6aacaa3c119 100644
--- a/spec/models/work_item_spec.rb
+++ b/spec/models/work_item_spec.rb
@@ -21,9 +21,8 @@ RSpec.describe WorkItem, feature_category: :portfolio_management do
.with_foreign_key('work_item_id')
end
- it 'has many `work_item_children_by_created_at`' do
- is_expected.to have_many(:work_item_children_by_created_at)
- .order(created_at: :asc)
+ it 'has many `work_item_children_by_relative_position`' do
+ is_expected.to have_many(:work_item_children_by_relative_position)
.class_name('WorkItem')
.with_foreign_key('work_item_id')
end
@@ -35,6 +34,49 @@ RSpec.describe WorkItem, feature_category: :portfolio_management do
end
end
+ describe '.work_item_children_by_relative_position' do
+ subject { parent_item.reload.work_item_children_by_relative_position }
+
+ let_it_be(:parent_item) { create(:work_item, :objective, project: reusable_project) }
+ let_it_be(:oldest_item) { create(:work_item, :objective, created_at: 5.hours.ago, project: reusable_project) }
+ let_it_be(:middle_item) { create(:work_item, :objective, project: reusable_project) }
+ let_it_be(:newest_item) { create(:work_item, :objective, created_at: 5.hours.from_now, project: reusable_project) }
+
+ let_it_be_with_reload(:link_to_oldest_item) do
+ create(:parent_link, work_item_parent: parent_item, work_item: oldest_item)
+ end
+
+ let_it_be_with_reload(:link_to_middle_item) do
+ create(:parent_link, work_item_parent: parent_item, work_item: middle_item)
+ end
+
+ let_it_be_with_reload(:link_to_newest_item) do
+ create(:parent_link, work_item_parent: parent_item, work_item: newest_item)
+ end
+
+ context 'when ordered by relative position and created_at' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:oldest_item_position, :middle_item_position, :newest_item_position, :expected_order) do
+ nil | nil | nil | lazy { [oldest_item, middle_item, newest_item] }
+ nil | nil | 1 | lazy { [newest_item, oldest_item, middle_item] }
+ nil | 1 | 2 | lazy { [middle_item, newest_item, oldest_item] }
+ 2 | 3 | 1 | lazy { [newest_item, oldest_item, middle_item] }
+ 1 | 2 | 3 | lazy { [oldest_item, middle_item, newest_item] }
+ end
+
+ with_them do
+ before do
+ link_to_oldest_item.update!(relative_position: oldest_item_position)
+ link_to_middle_item.update!(relative_position: middle_item_position)
+ link_to_newest_item.update!(relative_position: newest_item_position)
+ end
+
+ it { is_expected.to eq(expected_order) }
+ end
+ end
+ end
+
describe '#noteable_target_type_name' do
it 'returns `issue` as the target name' do
work_item = build(:work_item)
@@ -57,6 +99,70 @@ RSpec.describe WorkItem, feature_category: :portfolio_management do
end
end
+ describe '#supports_assignee?' do
+ let(:work_item) { build(:work_item, :task) }
+
+ before do
+ allow(work_item.work_item_type).to receive(:supports_assignee?).and_return(false)
+ end
+
+ it 'delegates the call to its work item type' do
+ expect(work_item.supports_assignee?).to be(false)
+ end
+ end
+
+ describe '#supported_quick_action_commands' do
+ let(:work_item) { build(:work_item, :task) }
+
+ subject { work_item.supported_quick_action_commands }
+
+ it 'returns quick action commands supported for all work items' do
+ is_expected.to include(:title, :reopen, :close, :cc, :tableflip, :shrug)
+ end
+
+ context 'when work item supports the assignee widget' do
+ it 'returns assignee related quick action commands' do
+ is_expected.to include(:assign, :unassign, :reassign)
+ end
+ end
+
+ context 'when work item does not the assignee widget' do
+ let(:work_item) { build(:work_item, :incident) }
+
+ it 'omits assignee related quick action commands' do
+ is_expected.not_to include(:assign, :unassign, :reassign)
+ end
+ end
+
+ context 'when work item supports the labels widget' do
+ it 'returns labels related quick action commands' do
+ is_expected.to include(:label, :labels, :relabel, :remove_label, :unlabel)
+ end
+ end
+
+ context 'when work item does not support the labels widget' do
+ let(:work_item) { build(:work_item, :incident) }
+
+ it 'omits labels related quick action commands' do
+ is_expected.not_to include(:label, :labels, :relabel, :remove_label, :unlabel)
+ end
+ end
+
+ context 'when work item supports the start and due date widget' do
+ it 'returns due date related quick action commands' do
+ is_expected.to include(:due, :remove_due_date)
+ end
+ end
+
+ context 'when work item does not support the start and due date widget' do
+ let(:work_item) { build(:work_item, :incident) }
+
+ it 'omits due date related quick action commands' do
+ is_expected.not_to include(:due, :remove_due_date)
+ end
+ end
+ end
+
describe 'callbacks' do
describe 'record_create_action' do
it 'records the creation action after saving' do
diff --git a/spec/models/work_items/type_spec.rb b/spec/models/work_items/type_spec.rb
index 1ada783385e..e5c88634b26 100644
--- a/spec/models/work_items/type_spec.rb
+++ b/spec/models/work_items/type_spec.rb
@@ -10,6 +10,20 @@ RSpec.describe WorkItems::Type do
describe 'associations' do
it { is_expected.to have_many(:work_items).with_foreign_key('work_item_type_id') }
it { is_expected.to belong_to(:namespace) }
+
+ it 'has many `widget_definitions`' do
+ is_expected.to have_many(:widget_definitions)
+ .class_name('::WorkItems::WidgetDefinition')
+ .with_foreign_key('work_item_type_id')
+ end
+
+ it 'has many `enabled_widget_definitions`' do
+ type = create(:work_item_type)
+ widget1 = create(:widget_definition, work_item_type: type)
+ create(:widget_definition, work_item_type: type, disabled: true)
+
+ expect(type.enabled_widget_definitions).to match_array([widget1])
+ end
end
describe 'scopes' do
@@ -60,29 +74,14 @@ RSpec.describe WorkItems::Type do
it { is_expected.not_to allow_value('s' * 256).for(:icon_name) }
end
- describe '.available_widgets' do
- subject { described_class.available_widgets }
-
- it 'returns list of all possible widgets' do
- is_expected.to include(
- ::WorkItems::Widgets::Description,
- ::WorkItems::Widgets::Hierarchy,
- ::WorkItems::Widgets::Labels,
- ::WorkItems::Widgets::Assignees,
- ::WorkItems::Widgets::StartAndDueDate,
- ::WorkItems::Widgets::Milestone,
- ::WorkItems::Widgets::Notes
- )
- end
- end
-
describe '.default_by_type' do
let(:default_issue_type) { described_class.find_by(namespace_id: nil, base_type: :issue) }
subject { described_class.default_by_type(:issue) }
it 'returns default work item type by base type without calling importer' do
- expect(Gitlab::DatabaseImporters::WorkItems::BaseTypeImporter).not_to receive(:upsert_types)
+ expect(Gitlab::DatabaseImporters::WorkItems::BaseTypeImporter).not_to receive(:upsert_types).and_call_original
+ expect(Gitlab::DatabaseImporters::WorkItems::BaseTypeImporter).not_to receive(:upsert_widgets)
expect(Gitlab::DatabaseImporters::WorkItems::HierarchyRestrictionsImporter).not_to receive(:upsert_restrictions)
expect(subject).to eq(default_issue_type)
@@ -94,7 +93,8 @@ RSpec.describe WorkItems::Type do
end
it 'creates types and restrictions and returns default work item type by base type' do
- expect(Gitlab::DatabaseImporters::WorkItems::BaseTypeImporter).to receive(:upsert_types)
+ expect(Gitlab::DatabaseImporters::WorkItems::BaseTypeImporter).to receive(:upsert_types).and_call_original
+ expect(Gitlab::DatabaseImporters::WorkItems::BaseTypeImporter).to receive(:upsert_widgets)
expect(Gitlab::DatabaseImporters::WorkItems::HierarchyRestrictionsImporter).to receive(:upsert_restrictions)
expect(subject).to eq(default_issue_type)
@@ -126,4 +126,41 @@ RSpec.describe WorkItems::Type do
expect(work_item_type.name).to eq('label😸')
end
end
+
+ describe '#supports_assignee?' do
+ let_it_be_with_reload(:work_item_type) { create(:work_item_type) }
+ let_it_be_with_reload(:widget_definition) do
+ create(:widget_definition, work_item_type: work_item_type, widget_type: :assignees)
+ end
+
+ subject(:supports_assignee) { work_item_type.supports_assignee? }
+
+ it { is_expected.to be_truthy }
+
+ context 'when the assignees widget is not supported' do
+ before do
+ widget_definition.update!(disabled: true)
+ end
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ describe '#default_issue?' do
+ context 'when work item type is default Issue' do
+ let(:work_item_type) { build(:work_item_type, name: described_class::TYPE_NAMES[:issue]) }
+
+ it 'returns true' do
+ expect(work_item_type.default_issue?).to be(true)
+ end
+ end
+
+ context 'when work item type is not Issue' do
+ let(:work_item_type) { build(:work_item_type) }
+
+ it 'returns false' do
+ expect(work_item_type.default_issue?).to be(false)
+ end
+ end
+ end
end
diff --git a/spec/models/work_items/widget_definition_spec.rb b/spec/models/work_items/widget_definition_spec.rb
new file mode 100644
index 00000000000..08f8f4d9663
--- /dev/null
+++ b/spec/models/work_items/widget_definition_spec.rb
@@ -0,0 +1,92 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe WorkItems::WidgetDefinition, feature_category: :team_planning do
+ let(:all_widget_classes) do
+ list = [
+ ::WorkItems::Widgets::Description,
+ ::WorkItems::Widgets::Hierarchy,
+ ::WorkItems::Widgets::Labels,
+ ::WorkItems::Widgets::Assignees,
+ ::WorkItems::Widgets::StartAndDueDate,
+ ::WorkItems::Widgets::Milestone,
+ ::WorkItems::Widgets::Notes
+ ]
+
+ if Gitlab.ee?
+ list += [
+ ::WorkItems::Widgets::Iteration,
+ ::WorkItems::Widgets::Weight,
+ ::WorkItems::Widgets::Status,
+ ::WorkItems::Widgets::HealthStatus,
+ ::WorkItems::Widgets::Progress,
+ ::WorkItems::Widgets::RequirementLegacy,
+ ::WorkItems::Widgets::TestReports
+ ]
+ end
+
+ list
+ end
+
+ describe 'associations' do
+ it { is_expected.to belong_to(:namespace) }
+ it { is_expected.to belong_to(:work_item_type) }
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:name) }
+ it { is_expected.to validate_uniqueness_of(:name).case_insensitive.scoped_to([:namespace_id, :work_item_type_id]) }
+ it { is_expected.to validate_length_of(:name).is_at_most(255) }
+ end
+
+ context 'with some widgets disabled' do
+ before do
+ described_class.global.where(widget_type: :notes).update_all(disabled: true)
+ end
+
+ describe '.available_widgets' do
+ subject { described_class.available_widgets }
+
+ it 'returns all global widgets excluding the disabled ones' do
+ # WorkItems::Widgets::Notes is excluded from widget class because:
+ # * although widget_definition below is enabled and uses notes widget, it's namespaced (has namespace != nil)
+ # * available_widgets takes into account only global definitions (which have namespace=nil)
+ namespace = create(:namespace)
+ create(:widget_definition, namespace: namespace, widget_type: :notes)
+
+ is_expected.to match_array(all_widget_classes - [::WorkItems::Widgets::Notes])
+ end
+
+ it 'returns all global widgets if there is at least one global widget definition which is enabled' do
+ create(:widget_definition, namespace: nil, widget_type: :notes)
+
+ is_expected.to match_array(all_widget_classes)
+ end
+ end
+
+ describe '.widget_classes' do
+ subject { described_class.widget_classes }
+
+ it 'returns all widget classes no matter if disabled or not' do
+ is_expected.to match_array(all_widget_classes)
+ end
+ end
+ end
+
+ describe '#widget_class' do
+ it 'returns widget class based on widget_type' do
+ expect(build(:widget_definition, widget_type: :description).widget_class).to eq(::WorkItems::Widgets::Description)
+ end
+
+ it 'returns nil if there is no class for the widget_type' do
+ described_class.first.update_column(:widget_type, -1)
+
+ expect(described_class.first.widget_class).to be_nil
+ end
+
+ it 'returns nil if there is no class for the widget_type' do
+ expect(build(:widget_definition, widget_type: nil).widget_class).to be_nil
+ end
+ end
+end
diff --git a/spec/models/work_items/widgets/assignees_spec.rb b/spec/models/work_items/widgets/assignees_spec.rb
index a2c93c07fde..19c17658ce4 100644
--- a/spec/models/work_items/widgets/assignees_spec.rb
+++ b/spec/models/work_items/widgets/assignees_spec.rb
@@ -11,6 +11,12 @@ RSpec.describe WorkItems::Widgets::Assignees do
it { is_expected.to eq(:assignees) }
end
+ describe '.quick_action_params' do
+ subject { described_class.quick_action_params }
+
+ it { is_expected.to include(:assignee_ids) }
+ end
+
describe '#type' do
subject { described_class.new(work_item).type }
diff --git a/spec/models/work_items/widgets/hierarchy_spec.rb b/spec/models/work_items/widgets/hierarchy_spec.rb
index 43670b30645..7ff3088d9ec 100644
--- a/spec/models/work_items/widgets/hierarchy_spec.rb
+++ b/spec/models/work_items/widgets/hierarchy_spec.rb
@@ -36,14 +36,40 @@ RSpec.describe WorkItems::Widgets::Hierarchy, feature_category: :team_planning d
it { is_expected.to contain_exactly(parent_link1.work_item, parent_link2.work_item) }
- context 'with default order by created_at' do
+ context 'when ordered by relative position and created_at' do
let_it_be(:oldest_child) { create(:work_item, :task, project: project, created_at: 5.minutes.ago) }
+ let_it_be(:newest_child) { create(:work_item, :task, project: project, created_at: 5.minutes.from_now) }
let_it_be_with_reload(:link_to_oldest_child) do
create(:parent_link, work_item_parent: work_item_parent, work_item: oldest_child)
end
- it { is_expected.to eq([link_to_oldest_child, parent_link1, parent_link2].map(&:work_item)) }
+ let_it_be_with_reload(:link_to_newest_child) do
+ create(:parent_link, work_item_parent: work_item_parent, work_item: newest_child)
+ end
+
+ let(:parent_links_ordered) { [link_to_oldest_child, parent_link1, parent_link2, link_to_newest_child] }
+
+ context 'when children relative positions are nil' do
+ it 'orders by created_at' do
+ is_expected.to eq(parent_links_ordered.map(&:work_item))
+ end
+ end
+
+ context 'when children relative positions are present' do
+ let(:first_position) { 10 }
+ let(:second_position) { 20 }
+ let(:parent_links_ordered) { [link_to_oldest_child, link_to_newest_child, parent_link1, parent_link2] }
+
+ before do
+ link_to_oldest_child.update!(relative_position: first_position)
+ link_to_newest_child.update!(relative_position: second_position)
+ end
+
+ it 'orders by relative_position and by created_at' do
+ is_expected.to eq(parent_links_ordered.map(&:work_item))
+ end
+ end
end
end
end
diff --git a/spec/models/work_items/widgets/labels_spec.rb b/spec/models/work_items/widgets/labels_spec.rb
index 15e8aaa1cf3..8640c39c146 100644
--- a/spec/models/work_items/widgets/labels_spec.rb
+++ b/spec/models/work_items/widgets/labels_spec.rb
@@ -11,6 +11,12 @@ RSpec.describe WorkItems::Widgets::Labels do
it { is_expected.to eq(:labels) }
end
+ describe '.quick_action_params' do
+ subject { described_class.quick_action_params }
+
+ it { is_expected.to include(:add_label_ids, :remove_label_ids, :label_ids) }
+ end
+
describe '#type' do
subject { described_class.new(work_item).type }
diff --git a/spec/models/work_items/widgets/start_and_due_date_spec.rb b/spec/models/work_items/widgets/start_and_due_date_spec.rb
index b023cc73e0f..568d960c9c7 100644
--- a/spec/models/work_items/widgets/start_and_due_date_spec.rb
+++ b/spec/models/work_items/widgets/start_and_due_date_spec.rb
@@ -11,6 +11,12 @@ RSpec.describe WorkItems::Widgets::StartAndDueDate do
it { is_expected.to eq(:start_and_due_date) }
end
+ describe '.quick_action_params' do
+ subject { described_class.quick_action_params }
+
+ it { is_expected.to include(:due_date) }
+ end
+
describe '#type' do
subject { described_class.new(work_item).type }