diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-02-20 16:49:51 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-02-20 16:49:51 +0300 |
commit | 71786ddc8e28fbd3cb3fcc4b3ff15e5962a1c82e (patch) | |
tree | 6a2d93ef3fb2d353bb7739e4b57e6541f51cdd71 /spec/models | |
parent | a7253423e3403b8c08f8a161e5937e1488f5f407 (diff) |
Add latest changes from gitlab-org/gitlab@15-9-stable-eev15.9.0-rc42
Diffstat (limited to 'spec/models')
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 } |