diff options
Diffstat (limited to 'spec/models')
55 files changed, 2329 insertions, 1234 deletions
diff --git a/spec/models/analytics/instance_statistics/measurement_spec.rb b/spec/models/analytics/usage_trends/measurement_spec.rb index dbb16c5ffbe..d9a6b70c87a 100644 --- a/spec/models/analytics/instance_statistics/measurement_spec.rb +++ b/spec/models/analytics/usage_trends/measurement_spec.rb @@ -2,9 +2,9 @@ require 'spec_helper' -RSpec.describe Analytics::InstanceStatistics::Measurement, type: :model do +RSpec.describe Analytics::UsageTrends::Measurement, type: :model do describe 'validation' do - let!(:measurement) { create(:instance_statistics_measurement) } + let!(:measurement) { create(:usage_trends_measurement) } it { is_expected.to validate_presence_of(:recorded_at) } it { is_expected.to validate_presence_of(:identifier) } @@ -33,9 +33,9 @@ RSpec.describe Analytics::InstanceStatistics::Measurement, type: :model do end describe 'scopes' do - let_it_be(:measurement_1) { create(:instance_statistics_measurement, :project_count, recorded_at: 10.days.ago) } - let_it_be(:measurement_2) { create(:instance_statistics_measurement, :project_count, recorded_at: 2.days.ago) } - let_it_be(:measurement_3) { create(:instance_statistics_measurement, :group_count, recorded_at: 5.days.ago) } + let_it_be(:measurement_1) { create(:usage_trends_measurement, :project_count, recorded_at: 10.days.ago) } + let_it_be(:measurement_2) { create(:usage_trends_measurement, :project_count, recorded_at: 2.days.ago) } + let_it_be(:measurement_3) { create(:usage_trends_measurement, :group_count, recorded_at: 5.days.ago) } describe '.order_by_latest' do subject { described_class.order_by_latest } @@ -101,15 +101,15 @@ RSpec.describe Analytics::InstanceStatistics::Measurement, type: :model do describe '.find_latest_or_fallback' do subject(:count) { described_class.find_latest_or_fallback(:pipelines_skipped).count } - context 'with instance statistics' do - let!(:measurement) { create(:instance_statistics_measurement, :pipelines_skipped_count) } + context 'with usage statistics' do + let!(:measurement) { create(:usage_trends_measurement, :pipelines_skipped_count) } it 'returns the latest stored measurement' do expect(count).to eq measurement.count end end - context 'without instance statistics' do + context 'without usage statistics' do it 'returns the realtime query of the measurement' do expect(count).to eq 0 end diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index 5658057f588..808932ce7e4 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -105,14 +105,14 @@ RSpec.describe ApplicationSetting do it { is_expected.not_to allow_value(false).for(:hashed_storage_enabled) } - it { is_expected.not_to allow_value(101).for(:repository_storages_weighted_default) } - it { is_expected.to allow_value('90').for(:repository_storages_weighted_default) } - it { is_expected.not_to allow_value(-1).for(:repository_storages_weighted_default) } - it { is_expected.to allow_value(100).for(:repository_storages_weighted_default) } - it { is_expected.to allow_value(0).for(:repository_storages_weighted_default) } - it { is_expected.to allow_value(50).for(:repository_storages_weighted_default) } - it { is_expected.to allow_value(nil).for(:repository_storages_weighted_default) } - it { is_expected.not_to allow_value({ default: 100, shouldntexist: 50 }).for(:repository_storages_weighted) } + it { is_expected.to allow_value('default' => 0).for(:repository_storages_weighted) } + it { is_expected.to allow_value('default' => 50).for(:repository_storages_weighted) } + it { is_expected.to allow_value('default' => 100).for(:repository_storages_weighted) } + it { is_expected.to allow_value('default' => '90').for(:repository_storages_weighted) } + it { is_expected.to allow_value('default' => nil).for(:repository_storages_weighted) } + it { is_expected.not_to allow_value('default' => -1).for(:repository_storages_weighted).with_message("value for 'default' must be between 0 and 100") } + it { is_expected.not_to allow_value('default' => 101).for(:repository_storages_weighted).with_message("value for 'default' must be between 0 and 100") } + it { is_expected.not_to allow_value('default' => 100, shouldntexist: 50).for(:repository_storages_weighted).with_message("can't include: shouldntexist") } it { is_expected.to allow_value(400).for(:notes_create_limit) } it { is_expected.not_to allow_value('two').for(:notes_create_limit) } @@ -377,7 +377,7 @@ RSpec.describe ApplicationSetting do end end - it_behaves_like 'an object with email-formated attributes', :abuse_notification_email do + it_behaves_like 'an object with email-formatted attributes', :abuse_notification_email do subject { setting } end @@ -984,12 +984,6 @@ RSpec.describe ApplicationSetting do it_behaves_like 'application settings examples' - describe 'repository_storages_weighted_attributes' do - it 'returns the keys for repository_storages_weighted' do - expect(subject.class.repository_storages_weighted_attributes).to eq([:repository_storages_weighted_default]) - end - end - describe 'kroki_format_supported?' do it 'returns true when Excalidraw is enabled' do subject.kroki_formats_excalidraw = true @@ -1033,11 +1027,4 @@ RSpec.describe ApplicationSetting do expect(subject.kroki_formats_excalidraw).to eq(true) end end - - it 'does not allow to set weight for non existing storage' do - setting.repository_storages_weighted = { invalid_storage: 100 } - - expect(setting).not_to be_valid - expect(setting.errors.messages[:repository_storages_weighted]).to match_array(["can't include: invalid_storage"]) - end end diff --git a/spec/models/bulk_imports/entity_spec.rb b/spec/models/bulk_imports/entity_spec.rb index e5ab96ca514..17ab4d5954c 100644 --- a/spec/models/bulk_imports/entity_spec.rb +++ b/spec/models/bulk_imports/entity_spec.rb @@ -14,7 +14,6 @@ RSpec.describe BulkImports::Entity, type: :model do it { is_expected.to validate_presence_of(:source_type) } it { is_expected.to validate_presence_of(:source_full_path) } it { is_expected.to validate_presence_of(:destination_name) } - it { is_expected.to validate_presence_of(:destination_namespace) } it { is_expected.to define_enum_for(:source_type).with_values(%i[group_entity project_entity]) } @@ -38,7 +37,11 @@ RSpec.describe BulkImports::Entity, type: :model do context 'when associated with a group and no project' do it 'is valid as a group_entity' do entity = build(:bulk_import_entity, :group_entity, group: build(:group), project: nil) + expect(entity).to be_valid + end + it 'is valid when destination_namespace is empty' do + entity = build(:bulk_import_entity, :group_entity, group: build(:group), project: nil, destination_namespace: '') expect(entity).to be_valid end @@ -57,6 +60,12 @@ 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 + expect(entity.errors).to include(:destination_namespace) + end + it 'is invalid as a project_entity' do entity = build(:bulk_import_entity, :group_entity, group: nil, project: build(:project)) @@ -94,7 +103,9 @@ RSpec.describe BulkImports::Entity, type: :model do ) expect(entity).not_to be_valid - expect(entity.errors).to include(:destination_namespace) + expect(entity.errors).to include(:base) + expect(entity.errors[:base]) + .to include('Import failed: Destination cannot be a subgroup of the source group. Change the destination and try again.') end it 'is invalid if destination namespace is a descendant of the source' do @@ -109,7 +120,8 @@ RSpec.describe BulkImports::Entity, type: :model do ) expect(entity).not_to be_valid - expect(entity.errors).to include(:destination_namespace) + expect(entity.errors[:base]) + .to include('Import failed: Destination cannot be a subgroup of the source group. Change the destination and try again.') end end end diff --git a/spec/models/ci/bridge_spec.rb b/spec/models/ci/bridge_spec.rb index b50e4204e0a..f3029598b02 100644 --- a/spec/models/ci/bridge_spec.rb +++ b/spec/models/ci/bridge_spec.rb @@ -41,7 +41,7 @@ RSpec.describe Ci::Bridge do end end - describe '#scoped_variables_hash' do + describe '#scoped_variables' do it 'returns a hash representing variables' do variables = %w[ CI_JOB_NAME CI_JOB_STAGE CI_COMMIT_SHA CI_COMMIT_SHORT_SHA @@ -53,7 +53,7 @@ RSpec.describe Ci::Bridge do CI_COMMIT_TIMESTAMP ] - expect(bridge.scoped_variables_hash.keys).to include(*variables) + expect(bridge.scoped_variables.map { |v| v[:key] }).to include(*variables) end context 'when bridge has dependency which has dotenv variable' do @@ -63,7 +63,7 @@ RSpec.describe Ci::Bridge do let!(:job_variable) { create(:ci_job_variable, :dotenv_source, job: test) } it 'includes inherited variable' do - expect(bridge.scoped_variables_hash).to include(job_variable.key => job_variable.value) + expect(bridge.scoped_variables.to_hash).to include(job_variable.key => job_variable.value) end end end diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 4ad7ce70a44..811decf42d7 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -581,7 +581,7 @@ RSpec.describe Ci::Build do end it 'that cannot handle build' do - expect_any_instance_of(Ci::Runner).to receive(:can_pick?).and_return(false) + expect_any_instance_of(Ci::Runner).to receive(:matches_build?).with(build).and_return(false) is_expected.to be_falsey end end @@ -817,7 +817,48 @@ RSpec.describe Ci::Build do end describe '#cache' do - let(:options) { { cache: { key: "key", paths: ["public"], policy: "pull-push" } } } + let(:options) do + { cache: [{ key: "key", paths: ["public"], policy: "pull-push" }] } + end + + context 'with multiple_cache_per_job FF disabled' do + before do + stub_feature_flags(multiple_cache_per_job: false) + end + let(:options) { { cache: { key: "key", paths: ["public"], policy: "pull-push" } } } + + subject { build.cache } + + context 'when build has cache' do + before do + allow(build).to receive(:options).and_return(options) + end + + context 'when project has jobs_cache_index' do + before do + allow_any_instance_of(Project).to receive(:jobs_cache_index).and_return(1) + end + + it { is_expected.to be_an(Array).and all(include(key: "key-1")) } + end + + context 'when project does not have jobs_cache_index' do + before do + allow_any_instance_of(Project).to receive(:jobs_cache_index).and_return(nil) + end + + it { is_expected.to eq([options[:cache]]) } + end + end + + context 'when build does not have cache' do + before do + allow(build).to receive(:options).and_return({}) + end + + it { is_expected.to eq([]) } + end + end subject { build.cache } @@ -826,6 +867,21 @@ RSpec.describe Ci::Build do allow(build).to receive(:options).and_return(options) end + context 'when build has multiple caches' do + let(:options) do + { cache: [ + { key: "key", paths: ["public"], policy: "pull-push" }, + { key: "key2", paths: ["public"], policy: "pull-push" } + ] } + end + + before do + allow_any_instance_of(Project).to receive(:jobs_cache_index).and_return(1) + end + + it { is_expected.to match([a_hash_including(key: "key-1"), a_hash_including(key: "key2-1")]) } + end + context 'when project has jobs_cache_index' do before do allow_any_instance_of(Project).to receive(:jobs_cache_index).and_return(1) @@ -839,7 +895,7 @@ RSpec.describe Ci::Build do allow_any_instance_of(Project).to receive(:jobs_cache_index).and_return(nil) end - it { is_expected.to eq([options[:cache]]) } + it { is_expected.to eq(options[:cache]) } end end @@ -848,7 +904,7 @@ RSpec.describe Ci::Build do allow(build).to receive(:options).and_return({}) end - it { is_expected.to eq([nil]) } + it { is_expected.to be_empty } end end @@ -1205,6 +1261,21 @@ RSpec.describe Ci::Build do end end + describe '#environment_deployment_tier' do + subject { build.environment_deployment_tier } + + let(:build) { described_class.new(options: options) } + let(:options) { { environment: { deployment_tier: 'production' } } } + + it { is_expected.to eq('production') } + + context 'when options does not include deployment_tier' do + let(:options) { { environment: { name: 'production' } } } + + it { is_expected.to be_nil } + end + end + describe 'deployment' do describe '#outdated_deployment?' do subject { build.outdated_deployment? } @@ -2367,6 +2438,7 @@ RSpec.describe Ci::Build do { key: 'CI_JOB_ID', value: build.id.to_s, public: true, masked: false }, { key: 'CI_JOB_URL', value: project.web_url + "/-/jobs/#{build.id}", public: true, masked: false }, { key: 'CI_JOB_TOKEN', value: 'my-token', public: false, masked: true }, + { key: 'CI_JOB_STARTED_AT', value: build.started_at&.iso8601, public: true, masked: false }, { key: 'CI_BUILD_ID', value: build.id.to_s, public: true, masked: false }, { key: 'CI_BUILD_TOKEN', value: 'my-token', public: false, masked: true }, { key: 'CI_REGISTRY_USER', value: 'gitlab-ci-token', public: true, masked: false }, @@ -2405,17 +2477,18 @@ RSpec.describe Ci::Build do { key: 'CI_PROJECT_REPOSITORY_LANGUAGES', value: project.repository_languages.map(&:name).join(',').downcase, public: true, masked: false }, { key: 'CI_DEFAULT_BRANCH', value: project.default_branch, public: true, masked: false }, { key: 'CI_PROJECT_CONFIG_PATH', value: project.ci_config_path_or_default, public: true, masked: false }, + { key: 'CI_CONFIG_PATH', value: project.ci_config_path_or_default, public: true, masked: false }, { key: 'CI_PAGES_DOMAIN', value: Gitlab.config.pages.host, public: true, masked: false }, { key: 'CI_PAGES_URL', value: project.pages_url, public: true, masked: false }, - { key: 'CI_DEPENDENCY_PROXY_SERVER', value: "#{Gitlab.config.gitlab.host}:#{Gitlab.config.gitlab.port}", public: true, masked: false }, + { key: 'CI_DEPENDENCY_PROXY_SERVER', value: Gitlab.host_with_port, public: true, masked: false }, { key: 'CI_DEPENDENCY_PROXY_GROUP_IMAGE_PREFIX', - value: "#{Gitlab.config.gitlab.host}:#{Gitlab.config.gitlab.port}/#{project.namespace.root_ancestor.path}#{DependencyProxy::URL_SUFFIX}", + value: "#{Gitlab.host_with_port}/#{project.namespace.root_ancestor.path}#{DependencyProxy::URL_SUFFIX}", public: true, masked: false }, { key: 'CI_API_V4_URL', value: 'http://localhost/api/v4', public: true, masked: false }, { key: 'CI_PIPELINE_IID', value: pipeline.iid.to_s, public: true, masked: false }, { key: 'CI_PIPELINE_SOURCE', value: pipeline.source, public: true, masked: false }, - { key: 'CI_CONFIG_PATH', value: pipeline.config_path, public: true, masked: false }, + { key: 'CI_PIPELINE_CREATED_AT', value: pipeline.created_at.iso8601, public: true, masked: false }, { key: 'CI_COMMIT_SHA', value: build.sha, public: true, masked: false }, { key: 'CI_COMMIT_SHORT_SHA', value: build.short_sha, public: true, masked: false }, { key: 'CI_COMMIT_BEFORE_SHA', value: build.before_sha, public: true, masked: false }, @@ -2440,7 +2513,8 @@ RSpec.describe Ci::Build do build.yaml_variables = [] end - it { is_expected.to eq(predefined_variables) } + it { is_expected.to be_instance_of(Gitlab::Ci::Variables::Collection) } + it { expect(subject.to_runner_variables).to eq(predefined_variables) } context 'when ci_job_jwt feature flag is disabled' do before do @@ -2495,7 +2569,7 @@ RSpec.describe Ci::Build do end it 'returns variables in order depending on resource hierarchy' do - is_expected.to eq( + expect(subject.to_runner_variables).to eq( [dependency_proxy_var, job_jwt_var, build_pre_var, @@ -2525,7 +2599,7 @@ RSpec.describe Ci::Build do end it 'matches explicit variables ordering' do - received_variables = subject.map { |variable| variable.fetch(:key) } + received_variables = subject.map { |variable| variable[:key] } expect(received_variables).to eq expected_variables end @@ -2584,14 +2658,14 @@ RSpec.describe Ci::Build do end shared_examples 'containing environment variables' do - it { environment_variables.each { |v| is_expected.to include(v) } } + it { is_expected.to include(*environment_variables) } end context 'when no URL was set' do it_behaves_like 'containing environment variables' it 'does not have CI_ENVIRONMENT_URL' do - keys = subject.map { |var| var[:key] } + keys = subject.pluck(:key) expect(keys).not_to include('CI_ENVIRONMENT_URL') end @@ -2618,7 +2692,7 @@ RSpec.describe Ci::Build do it_behaves_like 'containing environment variables' it 'puts $CI_ENVIRONMENT_URL in the last so all other variables are available to be used when runners are trying to expand it' do - expect(subject.last).to eq(environment_variables.last) + expect(subject.to_runner_variables.last).to eq(environment_variables.last) end end end @@ -2951,7 +3025,7 @@ RSpec.describe Ci::Build do end it 'overrides YAML variable using a pipeline variable' do - variables = subject.reverse.uniq { |variable| variable[:key] }.reverse + variables = subject.to_runner_variables.reverse.uniq { |variable| variable[:key] }.reverse expect(variables) .not_to include(key: 'MYVAR', value: 'myvar', public: true, masked: false) @@ -3248,47 +3322,6 @@ RSpec.describe Ci::Build do end end - describe '#scoped_variables_hash' do - context 'when overriding CI variables' do - before do - project.variables.create!(key: 'MY_VAR', value: 'my value 1') - pipeline.variables.create!(key: 'MY_VAR', value: 'my value 2') - end - - it 'returns a regular hash created using valid ordering' do - expect(build.scoped_variables_hash).to include('MY_VAR': 'my value 2') - expect(build.scoped_variables_hash).not_to include('MY_VAR': 'my value 1') - end - end - - context 'when overriding user-provided variables' do - let(:build) do - create(:ci_build, pipeline: pipeline, yaml_variables: [{ key: 'MY_VAR', value: 'myvar', public: true }]) - end - - before do - pipeline.variables.build(key: 'MY_VAR', value: 'pipeline value') - end - - it 'returns a hash including variable with higher precedence' do - expect(build.scoped_variables_hash).to include('MY_VAR': 'pipeline value') - expect(build.scoped_variables_hash).not_to include('MY_VAR': 'myvar') - end - end - - context 'when overriding CI instance variables' do - before do - create(:ci_instance_variable, key: 'MY_VAR', value: 'my value 1') - group.variables.create!(key: 'MY_VAR', value: 'my value 2') - end - - it 'returns a regular hash created using valid ordering' do - expect(build.scoped_variables_hash).to include('MY_VAR': 'my value 2') - expect(build.scoped_variables_hash).not_to include('MY_VAR': 'my value 1') - end - end - end - describe '#any_unmet_prerequisites?' do let(:build) { create(:ci_build, :created) } diff --git a/spec/models/ci/daily_build_group_report_result_spec.rb b/spec/models/ci/daily_build_group_report_result_spec.rb index f6e6a6a5e02..4e96ec7cecb 100644 --- a/spec/models/ci/daily_build_group_report_result_spec.rb +++ b/spec/models/ci/daily_build_group_report_result_spec.rb @@ -162,39 +162,5 @@ RSpec.describe Ci::DailyBuildGroupReportResult do end end end - - describe '.by_date' do - subject(:coverages) { described_class.by_date(start_date) } - - let!(:coverage_1) { create(:ci_daily_build_group_report_result, date: 1.week.ago) } - - context 'when project has several coverage' do - let!(:coverage_2) { create(:ci_daily_build_group_report_result, date: 2.weeks.ago) } - let(:start_date) { 1.week.ago.to_date.to_s } - - it 'returns the coverage from the start_date' do - expect(coverages).to contain_exactly(coverage_1) - end - end - - context 'when start_date is over 90 days' do - let!(:coverage_2) { create(:ci_daily_build_group_report_result, date: 90.days.ago) } - let!(:coverage_3) { create(:ci_daily_build_group_report_result, date: 91.days.ago) } - let(:start_date) { 1.year.ago.to_date.to_s } - - it 'returns the coverage in the last 90 days' do - expect(coverages).to contain_exactly(coverage_1, coverage_2) - end - end - - context 'when start_date is not a string' do - let!(:coverage_2) { create(:ci_daily_build_group_report_result, date: 90.days.ago) } - let(:start_date) { 1.week.ago } - - it 'returns the coverage in the last 90 days' do - expect(coverages).to contain_exactly(coverage_1, coverage_2) - end - end - end end end diff --git a/spec/models/ci/group_variable_spec.rb b/spec/models/ci/group_variable_spec.rb index c8eac4d8765..f0eec549da7 100644 --- a/spec/models/ci/group_variable_spec.rb +++ b/spec/models/ci/group_variable_spec.rb @@ -9,7 +9,17 @@ RSpec.describe Ci::GroupVariable do it { is_expected.to include_module(Presentable) } it { is_expected.to include_module(Ci::Maskable) } - it { is_expected.to validate_uniqueness_of(:key).scoped_to(:group_id).with_message(/\(\w+\) has already been taken/) } + it { is_expected.to include_module(HasEnvironmentScope) } + it { is_expected.to validate_uniqueness_of(:key).scoped_to([:group_id, :environment_scope]).with_message(/\(\w+\) has already been taken/) } + + describe '.by_environment_scope' do + let!(:matching_variable) { create(:ci_group_variable, environment_scope: 'production ') } + let!(:non_matching_variable) { create(:ci_group_variable, environment_scope: 'staging') } + + subject { Ci::GroupVariable.by_environment_scope('production') } + + it { is_expected.to contain_exactly(matching_variable) } + end describe '.unprotected' do subject { described_class.unprotected } diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 94943fb3644..d57a39d133f 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -8,12 +8,8 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do include Ci::SourcePipelineHelpers let_it_be(:user) { create(:user) } - let_it_be(:namespace) { create_default(:namespace) } - let_it_be(:project) { create_default(:project, :repository) } - - let(:pipeline) do - create(:ci_empty_pipeline, status: :created, project: project) - end + let_it_be(:namespace) { create_default(:namespace).freeze } + let_it_be(:project) { create_default(:project, :repository).freeze } it_behaves_like 'having unique enum values' @@ -53,6 +49,8 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end describe 'associations' do + let_it_be(:pipeline) { create(:ci_empty_pipeline, :created) } + it 'has a bidirectional relationship with projects' do expect(described_class.reflect_on_association(:project).has_inverse?).to eq(:all_pipelines) expect(Project.reflect_on_association(:all_pipelines).has_inverse?).to eq(:project) @@ -82,6 +80,8 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end describe '#set_status' do + let(:pipeline) { build(:ci_empty_pipeline, :created) } + where(:from_status, :to_status) do from_status_names = described_class.state_machines[:status].states.map(&:name) to_status_names = from_status_names - [:created] # we never want to transition into created @@ -105,6 +105,8 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end describe '.processables' do + let_it_be(:pipeline) { create(:ci_empty_pipeline, :created) } + before do create(:ci_build, name: 'build', pipeline: pipeline) create(:ci_bridge, name: 'bridge', pipeline: pipeline) @@ -142,7 +144,8 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do subject { described_class.for_sha(sha) } let(:sha) { 'abc' } - let!(:pipeline) { create(:ci_pipeline, sha: 'abc') } + + let_it_be(:pipeline) { create(:ci_pipeline, sha: 'abc') } it 'returns the pipeline' do is_expected.to contain_exactly(pipeline) @@ -170,7 +173,8 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do subject { described_class.for_source_sha(source_sha) } let(:source_sha) { 'abc' } - let!(:pipeline) { create(:ci_pipeline, source_sha: 'abc') } + + let_it_be(:pipeline) { create(:ci_pipeline, source_sha: 'abc') } it 'returns the pipeline' do is_expected.to contain_exactly(pipeline) @@ -228,7 +232,8 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do subject { described_class.for_branch(branch) } let(:branch) { 'master' } - let!(:pipeline) { create(:ci_pipeline, ref: 'master') } + + let_it_be(:pipeline) { create(:ci_pipeline, ref: 'master') } it 'returns the pipeline' do is_expected.to contain_exactly(pipeline) @@ -247,13 +252,16 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do describe '.ci_sources' do subject { described_class.ci_sources } - let!(:push_pipeline) { create(:ci_pipeline, source: :push) } - let!(:web_pipeline) { create(:ci_pipeline, source: :web) } - let!(:api_pipeline) { create(:ci_pipeline, source: :api) } - let!(:webide_pipeline) { create(:ci_pipeline, source: :webide) } - let!(:child_pipeline) { create(:ci_pipeline, source: :parent_pipeline) } + let(:push_pipeline) { build(:ci_pipeline, source: :push) } + let(:web_pipeline) { build(:ci_pipeline, source: :web) } + let(:api_pipeline) { build(:ci_pipeline, source: :api) } + let(:webide_pipeline) { build(:ci_pipeline, source: :webide) } + let(:child_pipeline) { build(:ci_pipeline, source: :parent_pipeline) } + let(:pipelines) { [push_pipeline, web_pipeline, api_pipeline, webide_pipeline, child_pipeline] } it 'contains pipelines having CI only sources' do + pipelines.map(&:save!) + expect(subject).to contain_exactly(push_pipeline, web_pipeline, api_pipeline) end @@ -365,8 +373,8 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end end - describe '#merge_request_pipeline?' do - subject { pipeline.merge_request_pipeline? } + describe '#merged_result_pipeline?' do + subject { pipeline.merged_result_pipeline? } let!(:pipeline) do create(:ci_pipeline, source: :merge_request_event, merge_request: merge_request, target_sha: target_sha) @@ -387,6 +395,8 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do describe '#merge_request_ref?' do subject { pipeline.merge_request_ref? } + let(:pipeline) { build(:ci_empty_pipeline, :created) } + it 'calls MergeRequest#merge_request_ref?' do expect(MergeRequest).to receive(:merge_request_ref?).with(pipeline.ref) @@ -606,7 +616,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do describe '#source' do context 'when creating new pipeline' do let(:pipeline) do - build(:ci_empty_pipeline, status: :created, project: project, source: nil) + build(:ci_empty_pipeline, :created, project: project, source: nil) end it "prevents from creating an object" do @@ -615,17 +625,21 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end context 'when updating existing pipeline' do + let(:pipeline) { create(:ci_empty_pipeline, :created) } + before do pipeline.update_attribute(:source, nil) end - it "object is valid" do + it 'object is valid' do expect(pipeline).to be_valid end end end describe '#block' do + let(:pipeline) { create(:ci_empty_pipeline, :created) } + it 'changes pipeline status to manual' do expect(pipeline.block).to be true expect(pipeline.reload).to be_manual @@ -636,7 +650,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do describe '#delay' do subject { pipeline.delay } - let(:pipeline) { build(:ci_pipeline, status: :created) } + let(:pipeline) { build(:ci_pipeline, :created) } it 'changes pipeline status to schedule' do subject @@ -646,6 +660,8 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end describe '#valid_commit_sha' do + let(:pipeline) { build_stubbed(:ci_empty_pipeline, :created, project: project) } + context 'commit.sha can not start with 00000000' do before do pipeline.sha = '0' * 40 @@ -659,6 +675,8 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do describe '#short_sha' do subject { pipeline.short_sha } + let(:pipeline) { build_stubbed(:ci_empty_pipeline, :created) } + it 'has 8 items' do expect(subject.size).to eq(8) end @@ -668,49 +686,58 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do describe '#retried' do subject { pipeline.retried } + let(:pipeline) { create(:ci_empty_pipeline, :created, project: project) } + let!(:build1) { create(:ci_build, pipeline: pipeline, name: 'deploy', retried: true) } + before do - @build1 = create(:ci_build, pipeline: pipeline, name: 'deploy', retried: true) - @build2 = create(:ci_build, pipeline: pipeline, name: 'deploy') + create(:ci_build, pipeline: pipeline, name: 'deploy') end it 'returns old builds' do - is_expected.to contain_exactly(@build1) + is_expected.to contain_exactly(build1) end end describe '#coverage' do - let(:project) { create(:project, build_coverage_regex: "/.*/") } - let(:pipeline) { create(:ci_empty_pipeline, project: project) } + let_it_be_with_reload(:pipeline) { create(:ci_empty_pipeline) } - it "calculates average when there are two builds with coverage" do - create(:ci_build, name: "rspec", coverage: 30, pipeline: pipeline) - create(:ci_build, name: "rubocop", coverage: 40, pipeline: pipeline) - expect(pipeline.coverage).to eq("35.00") - end + context 'with multiple pipelines' do + before_all do + create(:ci_build, name: "rspec", coverage: 30, pipeline: pipeline) + create(:ci_build, name: "rubocop", coverage: 40, pipeline: pipeline) + end - it "calculates average when there are two builds with coverage and one with nil" do - create(:ci_build, name: "rspec", coverage: 30, pipeline: pipeline) - create(:ci_build, name: "rubocop", coverage: 40, pipeline: pipeline) - create(:ci_build, pipeline: pipeline) - expect(pipeline.coverage).to eq("35.00") - end + it "calculates average when there are two builds with coverage" do + expect(pipeline.coverage).to eq("35.00") + end + + it "calculates average when there are two builds with coverage and one with nil" do + create(:ci_build, pipeline: pipeline) + + expect(pipeline.coverage).to eq("35.00") + end - it "calculates average when there are two builds with coverage and one is retried" do - create(:ci_build, name: "rspec", coverage: 30, pipeline: pipeline) - create(:ci_build, name: "rubocop", coverage: 30, pipeline: pipeline, retried: true) - create(:ci_build, name: "rubocop", coverage: 40, pipeline: pipeline) - expect(pipeline.coverage).to eq("35.00") + it "calculates average when there are two builds with coverage and one is retried" do + create(:ci_build, name: "rubocop", coverage: 30, pipeline: pipeline, retried: true) + + expect(pipeline.coverage).to eq("35.00") + end end - it "calculates average when there is one build without coverage" do - FactoryBot.create(:ci_build, pipeline: pipeline) - expect(pipeline.coverage).to be_nil + context 'when there is one build without coverage' do + it "calculates average to nil" do + create(:ci_build, pipeline: pipeline) + + expect(pipeline.coverage).to be_nil + end end end describe '#retryable?' do subject { pipeline.retryable? } + let_it_be(:pipeline) { create(:ci_empty_pipeline, :created, project: project) } + context 'no failed builds' do before do create_build('rspec', 'success') @@ -772,13 +799,15 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do 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_CONFIG_PATH + CI_PIPELINE_CREATED_AT CI_COMMIT_SHA CI_COMMIT_SHORT_SHA CI_COMMIT_BEFORE_SHA @@ -798,21 +827,18 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do 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, + create(:merge_request, :simple, source_project: project, - source_branch: 'feature', target_project: project, - target_branch: 'master', assignees: assignees, milestone: milestone, labels: labels) end - let(:assignees) { create_list(:user, 2) } - let(:milestone) { create(:milestone, project: project) } - let(:labels) { create_list(:label, 2) } - context 'when pipeline for merge request is created' do let(:pipeline) do create(:ci_pipeline, :detached_merge_request_pipeline, @@ -998,9 +1024,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end describe '#protected_ref?' do - before do - pipeline.project = create(:project, :repository) - end + let(:pipeline) { build(:ci_empty_pipeline, :created) } it 'delegates method to project' do expect(pipeline).not_to be_protected_ref @@ -1008,11 +1032,8 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end describe '#legacy_trigger' do - let(:trigger_request) { create(:ci_trigger_request) } - - before do - pipeline.trigger_requests << trigger_request - end + let(:trigger_request) { build(:ci_trigger_request) } + let(:pipeline) { build(:ci_empty_pipeline, :created, trigger_requests: [trigger_request]) } it 'returns first trigger request' do expect(pipeline.legacy_trigger).to eq trigger_request @@ -1022,6 +1043,8 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do describe '#auto_canceled?' do subject { pipeline.auto_canceled? } + let(:pipeline) { build(:ci_empty_pipeline, :created) } + context 'when it is canceled' do before do pipeline.cancel @@ -1029,7 +1052,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do context 'when there is auto_canceled_by' do before do - pipeline.update!(auto_canceled_by: create(:ci_empty_pipeline)) + pipeline.auto_canceled_by = create(:ci_empty_pipeline) end it 'is auto canceled' do @@ -1057,6 +1080,8 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end describe 'pipeline stages' do + let(:pipeline) { build(:ci_empty_pipeline, :created) } + describe 'legacy stages' do before do create(:commit_status, pipeline: pipeline, @@ -1107,22 +1132,28 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end context 'when commit status is retried' do - before do + let!(:old_commit_status) do create(:commit_status, pipeline: pipeline, - stage: 'build', - name: 'mac', - stage_idx: 0, - status: 'success') - - Ci::ProcessPipelineService - .new(pipeline) - .execute + stage: 'build', + name: 'mac', + stage_idx: 0, + status: 'success') end - it 'ignores the previous state' do - expect(statuses).to eq([%w(build success), - %w(test success), - %w(deploy running)]) + context 'when FF ci_remove_update_retried_from_process_pipeline is disabled' do + before do + stub_feature_flags(ci_remove_update_retried_from_process_pipeline: false) + + Ci::ProcessPipelineService + .new(pipeline) + .execute + end + + it 'ignores the previous state' do + expect(statuses).to eq([%w(build success), + %w(test success), + %w(deploy running)]) + end end end end @@ -1162,6 +1193,8 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do describe '#legacy_stage' do subject { pipeline.legacy_stage('test') } + let(:pipeline) { build(:ci_empty_pipeline, :created) } + context 'with status in stage' do before do create(:commit_status, pipeline: pipeline, stage: 'test') @@ -1184,6 +1217,8 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end describe '#stages' do + let(:pipeline) { build(:ci_empty_pipeline, :created) } + before do create(:ci_stage_entity, project: project, pipeline: pipeline, @@ -1238,6 +1273,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end describe 'state machine' do + let_it_be_with_reload(:pipeline) { create(:ci_empty_pipeline, :created) } let(:current) { Time.current.change(usec: 0) } let(:build) { create_build('build1', queued_at: 0) } let(:build_b) { create_build('build2', queued_at: 0) } @@ -1401,28 +1437,10 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end describe 'pipeline caching' do - context 'if pipeline is cacheable' do - before do - pipeline.source = 'push' - end - - it 'performs ExpirePipelinesCacheWorker' do - expect(ExpirePipelineCacheWorker).to receive(:perform_async).with(pipeline.id) + it 'performs ExpirePipelinesCacheWorker' do + expect(ExpirePipelineCacheWorker).to receive(:perform_async).with(pipeline.id) - pipeline.cancel - end - end - - context 'if pipeline is not cacheable' do - before do - pipeline.source = 'webide' - end - - it 'deos not perform ExpirePipelinesCacheWorker' do - expect(ExpirePipelineCacheWorker).not_to receive(:perform_async) - - pipeline.cancel - end + pipeline.cancel end end @@ -1441,24 +1459,25 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end describe 'auto merge' do - let(:merge_request) { create(:merge_request, :merge_when_pipeline_succeeds) } - - let(:pipeline) do - create(:ci_pipeline, :running, project: merge_request.source_project, - ref: merge_request.source_branch, - sha: merge_request.diff_head_sha) - end + context 'when auto merge is enabled' do + let_it_be_with_reload(:merge_request) { create(:merge_request, :merge_when_pipeline_succeeds) } + let_it_be_with_reload(:pipeline) do + create(:ci_pipeline, :running, project: merge_request.source_project, + ref: merge_request.source_branch, + sha: merge_request.diff_head_sha) + end - before do - merge_request.update_head_pipeline - end + before_all do + merge_request.update_head_pipeline + end - %w[succeed! drop! cancel! skip!].each do |action| - context "when the pipeline recieved #{action} event" do - it 'performs AutoMergeProcessWorker' do - expect(AutoMergeProcessWorker).to receive(:perform_async).with(merge_request.id) + %w[succeed! drop! cancel! skip!].each do |action| + context "when the pipeline recieved #{action} event" do + it 'performs AutoMergeProcessWorker' do + expect(AutoMergeProcessWorker).to receive(:perform_async).with(merge_request.id) - pipeline.public_send(action) + pipeline.public_send(action) + end end end end @@ -1610,15 +1629,15 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do context 'multi-project pipelines' do let!(:downstream_project) { create(:project, :repository) } - let!(:upstream_pipeline) { create(:ci_pipeline, project: project) } + let!(:upstream_pipeline) { create(:ci_pipeline) } let!(:downstream_pipeline) { create(:ci_pipeline, :with_job, project: downstream_project) } it_behaves_like 'upstream downstream pipeline' end context 'parent-child pipelines' do - let!(:upstream_pipeline) { create(:ci_pipeline, project: project) } - let!(:downstream_pipeline) { create(:ci_pipeline, :with_job, project: project) } + let!(:upstream_pipeline) { create(:ci_pipeline) } + let!(:downstream_pipeline) { create(:ci_pipeline, :with_job) } it_behaves_like 'upstream downstream pipeline' end @@ -1637,6 +1656,8 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do describe '#branch?' do subject { pipeline.branch? } + let(:pipeline) { build(:ci_empty_pipeline, :created) } + context 'when ref is not a tag' do before do pipeline.tag = false @@ -1647,16 +1668,12 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end context 'when pipeline is merge request' do - let(:pipeline) do - create(:ci_pipeline, merge_request: merge_request) - end + let(:pipeline) { build(:ci_pipeline, merge_request: merge_request) } let(:merge_request) do - create(:merge_request, + create(:merge_request, :simple, source_project: project, - source_branch: 'feature', - target_project: project, - target_branch: 'master') + target_project: project) end it 'returns false' do @@ -1720,6 +1737,8 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do context 'when repository exists' do using RSpec::Parameterized::TableSyntax + let_it_be(:pipeline, refind: true) { create(:ci_empty_pipeline) } + where(:tag, :ref, :result) do false | 'master' | true false | 'non-existent-branch' | false @@ -1728,8 +1747,8 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end with_them do - let(:pipeline) do - create(:ci_empty_pipeline, project: project, tag: tag, ref: ref) + before do + pipeline.update!(tag: tag, ref: ref) end it "correctly detects ref" do @@ -1739,10 +1758,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end context 'when repository does not exist' do - let(:project) { create(:project) } - let(:pipeline) do - create(:ci_empty_pipeline, project: project, ref: 'master') - end + let(:pipeline) { build(:ci_empty_pipeline, ref: 'master', project: build(:project)) } it 'always returns false' do expect(pipeline.ref_exists?).to eq false @@ -1753,7 +1769,6 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do context 'with non-empty project' do let(:pipeline) do create(:ci_pipeline, - project: project, ref: project.default_branch, sha: project.commit.sha) end @@ -1761,14 +1776,12 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do describe '#lazy_ref_commit' do let(:another) do create(:ci_pipeline, - project: project, ref: 'feature', sha: project.commit('feature').sha) end let(:unicode) do create(:ci_pipeline, - project: project, ref: 'ü/unicode/multi-byte') end @@ -1827,6 +1840,8 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do describe '#manual_actions' do subject { pipeline.manual_actions } + let(:pipeline) { create(:ci_empty_pipeline, :created) } + it 'when none defined' do is_expected.to be_empty end @@ -1853,9 +1868,11 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end describe '#branch_updated?' do + let(:pipeline) { create(:ci_empty_pipeline, :created) } + context 'when pipeline has before SHA' do before do - pipeline.update_column(:before_sha, 'a1b2c3d4') + pipeline.update!(before_sha: 'a1b2c3d4') end it 'runs on a branch update push' do @@ -1866,7 +1883,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do context 'when pipeline does not have before SHA' do before do - pipeline.update_column(:before_sha, Gitlab::Git::BLANK_SHA) + pipeline.update!(before_sha: Gitlab::Git::BLANK_SHA) end it 'does not run on a branch updating push' do @@ -1876,6 +1893,8 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end describe '#modified_paths' do + let(:pipeline) { create(:ci_empty_pipeline, :created) } + context 'when old and new revisions are set' do before do pipeline.update!(before_sha: '1234abcd', sha: '2345bcde') @@ -1892,7 +1911,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do context 'when either old or new revision is missing' do before do - pipeline.update_column(:before_sha, Gitlab::Git::BLANK_SHA) + pipeline.update!(before_sha: Gitlab::Git::BLANK_SHA) end it 'returns nil' do @@ -1906,11 +1925,9 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end let(:merge_request) do - create(:merge_request, + create(:merge_request, :simple, source_project: project, - source_branch: 'feature', - target_project: project, - target_branch: 'master') + target_project: project) end it 'returns merge request modified paths' do @@ -1944,6 +1961,8 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end describe '#has_kubernetes_active?' do + let(:pipeline) { create(:ci_empty_pipeline, :created, project: project) } + context 'when kubernetes is active' do context 'when user configured kubernetes from CI/CD > Clusters' do let!(:cluster) { create(:cluster, :project, :provided_by_gcp) } @@ -1965,6 +1984,8 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do describe '#has_warnings?' do subject { pipeline.has_warnings? } + let_it_be(:pipeline) { create(:ci_empty_pipeline, :created) } + context 'build which is allowed to fail fails' do before do create :ci_build, :success, pipeline: pipeline, name: 'rspec' @@ -2021,6 +2042,8 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end describe '#number_of_warnings' do + let_it_be(:pipeline) { create(:ci_empty_pipeline, :created) } + it 'returns the number of warnings' do create(:ci_build, :allowed_to_fail, :failed, pipeline: pipeline, name: 'rubocop') create(:ci_bridge, :allowed_to_fail, :failed, pipeline: pipeline, name: 'rubocop') @@ -2029,7 +2052,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end it 'supports eager loading of the number of warnings' do - pipeline2 = create(:ci_empty_pipeline, status: :created, project: project) + pipeline2 = create(:ci_empty_pipeline, :created) create(:ci_build, :allowed_to_fail, :failed, pipeline: pipeline, name: 'rubocop') create(:ci_build, :allowed_to_fail, :failed, pipeline: pipeline2, name: 'rubocop') @@ -2053,6 +2076,8 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do subject { pipeline.needs_processing? } + let_it_be(:pipeline) { create(:ci_empty_pipeline, :created) } + where(:processed, :result) do nil | true false | true @@ -2072,122 +2097,107 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end end - shared_context 'with some outdated pipelines' do - before do - create_pipeline(:canceled, 'ref', 'A', project) - create_pipeline(:success, 'ref', 'A', project) - create_pipeline(:failed, 'ref', 'B', project) - create_pipeline(:skipped, 'feature', 'C', project) + context 'with outdated pipelines' do + before_all do + create_pipeline(:canceled, 'ref', 'A') + create_pipeline(:success, 'ref', 'A') + create_pipeline(:failed, 'ref', 'B') + create_pipeline(:skipped, 'feature', 'C') end - def create_pipeline(status, ref, sha, project) + def create_pipeline(status, ref, sha) create( :ci_empty_pipeline, status: status, ref: ref, - sha: sha, - project: project + sha: sha ) end - end - describe '.newest_first' do - include_context 'with some outdated pipelines' - - it 'returns the pipelines from new to old' do - expect(described_class.newest_first.pluck(:status)) - .to eq(%w[skipped failed success canceled]) - end + describe '.newest_first' do + it 'returns the pipelines from new to old' do + expect(described_class.newest_first.pluck(:status)) + .to eq(%w[skipped failed success canceled]) + end - it 'searches limited backlog' do - expect(described_class.newest_first(limit: 1).pluck(:status)) - .to eq(%w[skipped]) + it 'searches limited backlog' do + expect(described_class.newest_first(limit: 1).pluck(:status)) + .to eq(%w[skipped]) + end end - end - describe '.latest_status' do - include_context 'with some outdated pipelines' - - context 'when no ref is specified' do - it 'returns the status of the latest pipeline' do - expect(described_class.latest_status).to eq('skipped') + describe '.latest_status' do + context 'when no ref is specified' do + it 'returns the status of the latest pipeline' do + expect(described_class.latest_status).to eq('skipped') + end end - end - context 'when ref is specified' do - it 'returns the status of the latest pipeline for the given ref' do - expect(described_class.latest_status('ref')).to eq('failed') + context 'when ref is specified' do + it 'returns the status of the latest pipeline for the given ref' do + expect(described_class.latest_status('ref')).to eq('failed') + end end end - end - describe '.latest_successful_for_ref' do - include_context 'with some outdated pipelines' - - let!(:latest_successful_pipeline) do - create_pipeline(:success, 'ref', 'D', project) - end + describe '.latest_successful_for_ref' do + let!(:latest_successful_pipeline) do + create_pipeline(:success, 'ref', 'D') + end - it 'returns the latest successful pipeline' do - expect(described_class.latest_successful_for_ref('ref')) - .to eq(latest_successful_pipeline) + it 'returns the latest successful pipeline' do + expect(described_class.latest_successful_for_ref('ref')) + .to eq(latest_successful_pipeline) + end end - end - describe '.latest_running_for_ref' do - include_context 'with some outdated pipelines' - - let!(:latest_running_pipeline) do - create_pipeline(:running, 'ref', 'D', project) - end + describe '.latest_running_for_ref' do + let!(:latest_running_pipeline) do + create_pipeline(:running, 'ref', 'D') + end - it 'returns the latest running pipeline' do - expect(described_class.latest_running_for_ref('ref')) - .to eq(latest_running_pipeline) + it 'returns the latest running pipeline' do + expect(described_class.latest_running_for_ref('ref')) + .to eq(latest_running_pipeline) + end end - end - - describe '.latest_failed_for_ref' do - include_context 'with some outdated pipelines' - let!(:latest_failed_pipeline) do - create_pipeline(:failed, 'ref', 'D', project) - end + describe '.latest_failed_for_ref' do + let!(:latest_failed_pipeline) do + create_pipeline(:failed, 'ref', 'D') + end - it 'returns the latest failed pipeline' do - expect(described_class.latest_failed_for_ref('ref')) - .to eq(latest_failed_pipeline) + it 'returns the latest failed pipeline' do + expect(described_class.latest_failed_for_ref('ref')) + .to eq(latest_failed_pipeline) + end end - end - - describe '.latest_successful_for_sha' do - include_context 'with some outdated pipelines' - let!(:latest_successful_pipeline) do - create_pipeline(:success, 'ref', 'awesomesha', project) - end + describe '.latest_successful_for_sha' do + let!(:latest_successful_pipeline) do + create_pipeline(:success, 'ref', 'awesomesha') + end - it 'returns the latest successful pipeline' do - expect(described_class.latest_successful_for_sha('awesomesha')) - .to eq(latest_successful_pipeline) + it 'returns the latest successful pipeline' do + expect(described_class.latest_successful_for_sha('awesomesha')) + .to eq(latest_successful_pipeline) + end end - end - - describe '.latest_successful_for_refs' do - include_context 'with some outdated pipelines' - let!(:latest_successful_pipeline1) do - create_pipeline(:success, 'ref1', 'D', project) - end + describe '.latest_successful_for_refs' do + let!(:latest_successful_pipeline1) do + create_pipeline(:success, 'ref1', 'D') + end - let!(:latest_successful_pipeline2) do - create_pipeline(:success, 'ref2', 'D', project) - end + let!(:latest_successful_pipeline2) do + create_pipeline(:success, 'ref2', 'D') + end - it 'returns the latest successful pipeline for both refs' do - refs = %w(ref1 ref2 ref3) + it 'returns the latest successful pipeline for both refs' do + refs = %w(ref1 ref2 ref3) - expect(described_class.latest_successful_for_refs(refs)).to eq({ 'ref1' => latest_successful_pipeline1, 'ref2' => latest_successful_pipeline2 }) + expect(described_class.latest_successful_for_refs(refs)).to eq({ 'ref1' => latest_successful_pipeline1, 'ref2' => latest_successful_pipeline2 }) + end end end @@ -2197,8 +2207,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do :ci_empty_pipeline, status: 'success', ref: 'master', - sha: '123', - project: project + sha: '123' ) end @@ -2207,8 +2216,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do :ci_empty_pipeline, status: 'success', ref: 'develop', - sha: '123', - project: project + sha: '123' ) end @@ -2217,8 +2225,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do :ci_empty_pipeline, status: 'success', ref: 'test', - sha: '456', - project: project + sha: '456' ) end @@ -2315,12 +2322,11 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end describe '#status', :sidekiq_inline do - let(:build) do - create(:ci_build, :created, pipeline: pipeline, name: 'test') - end - subject { pipeline.reload.status } + let_it_be(:pipeline) { create(:ci_empty_pipeline, :created) } + let(:build) { create(:ci_build, :created, pipeline: pipeline, name: 'test') } + context 'on waiting for resource' do before do allow(build).to receive(:with_resource_group?) { true } @@ -2412,8 +2418,10 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do describe '#detailed_status' do subject { pipeline.detailed_status(user) } + let_it_be(:pipeline) { create(:ci_empty_pipeline, :created) } + context 'when pipeline is created' do - let(:pipeline) { create(:ci_pipeline, status: :created) } + let(:pipeline) { create(:ci_pipeline, :created) } it 'returns detailed status for created pipeline' do expect(subject.text).to eq s_('CiStatusText|created') @@ -2490,6 +2498,8 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end describe '#cancelable?' do + let_it_be(:pipeline) { create(:ci_empty_pipeline, :created) } + %i[created running pending].each do |status0| context "when there is a build #{status0}" do before do @@ -2581,7 +2591,9 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end describe '#cancel_running' do - let(:latest_status) { pipeline.statuses.pluck(:status) } + subject(:latest_status) { pipeline.statuses.pluck(:status) } + + let_it_be(:pipeline) { create(:ci_empty_pipeline, :created) } context 'when there is a running external job and a regular job' do before do @@ -2624,7 +2636,9 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end describe '#retry_failed' do - let(:latest_status) { pipeline.latest_statuses.pluck(:status) } + subject(:latest_status) { pipeline.latest_statuses.pluck(:status) } + + let_it_be(:pipeline) { create(:ci_empty_pipeline, :created) } before do stub_not_protect_default_branch @@ -2673,11 +2687,12 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end describe '#execute_hooks' do + let_it_be(:pipeline) { create(:ci_empty_pipeline, :created) } let!(:build_a) { create_build('a', 0) } let!(:build_b) { create_build('b', 0) } let!(:hook) do - create(:project_hook, project: project, pipeline_events: enabled) + create(:project_hook, pipeline_events: enabled) end before do @@ -2703,7 +2718,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end it 'builds hook data once' do - create(:pipelines_email_service, project: project) + create(:pipelines_email_service) expect(Gitlab::DataBuilder::Pipeline).to receive(:build).once.and_call_original @@ -2789,7 +2804,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end describe "#merge_requests_as_head_pipeline" do - let(:pipeline) { create(:ci_empty_pipeline, status: 'created', project: project, ref: 'master', sha: 'a288a022a53a5a944fae87bcec6efc87b7061808') } + let_it_be_with_reload(:pipeline) { create(:ci_empty_pipeline, status: 'created', ref: 'master', sha: 'a288a022a53a5a944fae87bcec6efc87b7061808') } it "returns merge requests whose `diff_head_sha` matches the pipeline's SHA" do allow_next_instance_of(MergeRequest) do |instance| @@ -2801,7 +2816,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end it "doesn't return merge requests whose source branch doesn't match the pipeline's ref" do - create(:merge_request, source_project: project, source_branch: 'feature', target_branch: 'master') + create(:merge_request, :simple, source_project: project) expect(pipeline.merge_requests_as_head_pipeline).to be_empty end @@ -2817,7 +2832,8 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end describe '#all_merge_requests' do - let(:project) { create(:project) } + let_it_be_with_reload(:project) { create(:project) } + let_it_be(:pipeline) { create(:ci_empty_pipeline, :created, project: project) } shared_examples 'a method that returns all merge requests for a given pipeline' do let(:pipeline) { create(:ci_empty_pipeline, status: 'created', project: pipeline_project, ref: 'master') } @@ -2911,10 +2927,9 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end describe '#related_merge_requests' do - let(:project) { create(:project, :repository) } let(:merge_request) { create(:merge_request, source_project: project, source_branch: 'feature', target_branch: 'master') } let(:other_merge_request) { create(:merge_request, source_project: project, source_branch: 'feature', target_branch: 'stable') } - let(:branch_pipeline) { create(:ci_pipeline, project: project, ref: 'feature') } + let(:branch_pipeline) { create(:ci_pipeline, ref: 'feature') } let(:merge_pipeline) { create(:ci_pipeline, :detached_merge_request_pipeline, merge_request: merge_request) } context 'for a branch pipeline' do @@ -2951,9 +2966,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end describe '#open_merge_requests_refs' do - let(:project) { create(:project) } - let(:user) { create(:user) } - let!(:pipeline) { create(:ci_pipeline, user: user, project: project, ref: 'feature') } + let!(:pipeline) { create(:ci_pipeline, user: user, ref: 'feature') } let!(:merge_request) { create(:merge_request, source_project: project, source_branch: 'feature', target_branch: 'master') } subject { pipeline.open_merge_requests_refs } @@ -3000,6 +3013,8 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do describe '#same_family_pipeline_ids' do subject { pipeline.same_family_pipeline_ids.map(&:id) } + let_it_be(:pipeline) { create(:ci_empty_pipeline, :created) } + context 'when pipeline is not child nor parent' do it 'returns just the pipeline id' do expect(subject).to contain_exactly(pipeline.id) @@ -3007,7 +3022,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end context 'when pipeline is child' do - let(:parent) { create(:ci_pipeline, project: project) } + let(:parent) { create(:ci_pipeline) } let!(:pipeline) { create(:ci_pipeline, child_of: parent) } let!(:sibling) { create(:ci_pipeline, child_of: parent) } @@ -3025,7 +3040,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end context 'when pipeline is a child of a child pipeline' do - let(:ancestor) { create(:ci_pipeline, project: project) } + let(:ancestor) { create(:ci_pipeline) } let!(:parent) { create(:ci_pipeline, child_of: ancestor) } let!(:pipeline) { create(:ci_pipeline, child_of: parent) } let!(:cousin_parent) { create(:ci_pipeline, child_of: ancestor) } @@ -3050,10 +3065,10 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do describe '#root_ancestor' do subject { pipeline.root_ancestor } - let_it_be(:pipeline) { create(:ci_pipeline, project: project) } + let_it_be(:pipeline) { create(:ci_pipeline) } context 'when pipeline is child of child pipeline' do - let!(:root_ancestor) { create(:ci_pipeline, project: project) } + let!(:root_ancestor) { create(:ci_pipeline) } let!(:parent_pipeline) { create(:ci_pipeline, child_of: root_ancestor) } let!(:pipeline) { create(:ci_pipeline, child_of: parent_pipeline) } @@ -3088,6 +3103,8 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end describe '#stuck?' do + let(:pipeline) { create(:ci_empty_pipeline, :created) } + before do create(:ci_build, :pending, pipeline: pipeline) end @@ -3132,6 +3149,8 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end describe '#has_yaml_errors?' do + let(:pipeline) { build_stubbed(:ci_pipeline) } + context 'when yaml_errors is set' do before do pipeline.yaml_errors = 'File not found' @@ -3201,7 +3220,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do context 'when pipeline is not the latest' do before do - create(:ci_pipeline, :success, project: project, ci_ref: pipeline.ci_ref) + create(:ci_pipeline, :success, ci_ref: pipeline.ci_ref) end it 'does not pass ref_status' do @@ -3302,7 +3321,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do describe '#builds_in_self_and_descendants' do subject(:builds) { pipeline.builds_in_self_and_descendants } - let(:pipeline) { create(:ci_pipeline, project: project) } + let(:pipeline) { create(:ci_pipeline) } let!(:build) { create(:ci_build, pipeline: pipeline) } context 'when pipeline is standalone' do @@ -3333,6 +3352,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end describe '#build_with_artifacts_in_self_and_descendants' do + let_it_be(:pipeline) { create(:ci_pipeline) } let!(:build) { create(:ci_build, name: 'test', pipeline: pipeline) } let(:child_pipeline) { create(:ci_pipeline, child_of: pipeline) } let!(:child_build) { create(:ci_build, :artifacts, name: 'test', pipeline: child_pipeline) } @@ -3351,6 +3371,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end describe '#find_job_with_archive_artifacts' do + let(:pipeline) { create(:ci_pipeline) } let!(:old_job) { create(:ci_build, name: 'rspec', retried: true, pipeline: pipeline) } let!(:job_without_artifacts) { create(:ci_build, name: 'rspec', pipeline: pipeline) } let!(:expected_job) { create(:ci_build, :artifacts, name: 'rspec', pipeline: pipeline ) } @@ -3364,6 +3385,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end describe '#latest_builds_with_artifacts' do + let(:pipeline) { create(:ci_pipeline) } let!(:fresh_build) { create(:ci_build, :success, :artifacts, pipeline: pipeline) } let!(:stale_build) { create(:ci_build, :success, :expired, :artifacts, pipeline: pipeline) } @@ -3390,7 +3412,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do describe '#batch_lookup_report_artifact_for_file_type' do context 'with code quality report artifact' do - let(:pipeline) { create(:ci_pipeline, :with_codequality_reports, project: project) } + let(:pipeline) { create(:ci_pipeline, :with_codequality_reports) } it "returns the code quality artifact" do expect(pipeline.batch_lookup_report_artifact_for_file_type(:codequality)).to eq(pipeline.job_artifacts.sample) @@ -3399,24 +3421,26 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end describe '#latest_report_builds' do + let_it_be(:pipeline) { create(:ci_pipeline, project: project) } + it 'returns build with test artifacts' do - test_build = create(:ci_build, :test_reports, pipeline: pipeline, project: project) - coverage_build = create(:ci_build, :coverage_reports, pipeline: pipeline, project: project) + test_build = create(:ci_build, :test_reports, pipeline: pipeline) + coverage_build = create(:ci_build, :coverage_reports, pipeline: pipeline) create(:ci_build, :artifacts, pipeline: pipeline, project: project) expect(pipeline.latest_report_builds).to contain_exactly(test_build, coverage_build) end it 'filters builds by scope' do - test_build = create(:ci_build, :test_reports, pipeline: pipeline, project: project) - create(:ci_build, :coverage_reports, pipeline: pipeline, project: project) + test_build = create(:ci_build, :test_reports, pipeline: pipeline) + create(:ci_build, :coverage_reports, pipeline: pipeline) expect(pipeline.latest_report_builds(Ci::JobArtifact.test_reports)).to contain_exactly(test_build) end it 'only returns not retried builds' do - test_build = create(:ci_build, :test_reports, pipeline: pipeline, project: project) - create(:ci_build, :test_reports, :retried, pipeline: pipeline, project: project) + test_build = create(:ci_build, :test_reports, pipeline: pipeline) + create(:ci_build, :test_reports, :retried, pipeline: pipeline) expect(pipeline.latest_report_builds).to contain_exactly(test_build) end @@ -3427,17 +3451,17 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do context 'when pipeline has builds with test reports' do before do - create(:ci_build, :test_reports, pipeline: pipeline, project: project) + create(:ci_build, :test_reports, pipeline: pipeline) end context 'when pipeline status is running' do - let(:pipeline) { create(:ci_pipeline, :running, project: project) } + let(:pipeline) { create(:ci_pipeline, :running) } it { is_expected.to be_falsey } end context 'when pipeline status is success' do - let(:pipeline) { create(:ci_pipeline, :success, project: project) } + let(:pipeline) { create(:ci_pipeline, :success) } it { is_expected.to be_truthy } end @@ -3445,20 +3469,20 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do context 'when pipeline does not have builds with test reports' do before do - create(:ci_build, :artifacts, pipeline: pipeline, project: project) + create(:ci_build, :artifacts, pipeline: pipeline) end - let(:pipeline) { create(:ci_pipeline, :success, project: project) } + let(:pipeline) { create(:ci_pipeline, :success) } it { is_expected.to be_falsey } end context 'when retried build has test reports' do before do - create(:ci_build, :retried, :test_reports, pipeline: pipeline, project: project) + create(:ci_build, :retried, :test_reports, pipeline: pipeline) end - let(:pipeline) { create(:ci_pipeline, :success, project: project) } + let(:pipeline) { create(:ci_pipeline, :success) } it { is_expected.to be_falsey } end @@ -3468,13 +3492,13 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do subject { pipeline.has_coverage_reports? } context 'when pipeline has a code coverage artifact' do - let(:pipeline) { create(:ci_pipeline, :with_coverage_report_artifact, :running, project: project) } + let(:pipeline) { create(:ci_pipeline, :with_coverage_report_artifact, :running) } it { expect(subject).to be_truthy } end context 'when pipeline does not have a code coverage artifact' do - let(:pipeline) { create(:ci_pipeline, :success, project: project) } + let(:pipeline) { create(:ci_pipeline, :success) } it { expect(subject).to be_falsey } end @@ -3485,17 +3509,17 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do context 'when pipeline has builds with coverage reports' do before do - create(:ci_build, :coverage_reports, pipeline: pipeline, project: project) + create(:ci_build, :coverage_reports, pipeline: pipeline) end context 'when pipeline status is running' do - let(:pipeline) { create(:ci_pipeline, :running, project: project) } + let(:pipeline) { create(:ci_pipeline, :running) } it { expect(subject).to be_falsey } end context 'when pipeline status is success' do - let(:pipeline) { create(:ci_pipeline, :success, project: project) } + let(:pipeline) { create(:ci_pipeline, :success) } it { expect(subject).to be_truthy } end @@ -3503,10 +3527,10 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do context 'when pipeline does not have builds with coverage reports' do before do - create(:ci_build, :artifacts, pipeline: pipeline, project: project) + create(:ci_build, :artifacts, pipeline: pipeline) end - let(:pipeline) { create(:ci_pipeline, :success, project: project) } + let(:pipeline) { create(:ci_pipeline, :success) } it { expect(subject).to be_falsey } end @@ -3516,13 +3540,13 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do subject { pipeline.has_codequality_mr_diff_report? } context 'when pipeline has a codequality mr diff report' do - let(:pipeline) { create(:ci_pipeline, :with_codequality_mr_diff_report, :running, project: project) } + let(:pipeline) { create(:ci_pipeline, :with_codequality_mr_diff_report, :running) } it { expect(subject).to be_truthy } end context 'when pipeline does not have a codequality mr diff report' do - let(:pipeline) { create(:ci_pipeline, :success, project: project) } + let(:pipeline) { create(:ci_pipeline, :success) } it { expect(subject).to be_falsey } end @@ -3533,17 +3557,17 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do context 'when pipeline has builds with codequality reports' do before do - create(:ci_build, :codequality_reports, pipeline: pipeline, project: project) + create(:ci_build, :codequality_reports, pipeline: pipeline) end context 'when pipeline status is running' do - let(:pipeline) { create(:ci_pipeline, :running, project: project) } + let(:pipeline) { create(:ci_pipeline, :running) } it { expect(subject).to be_falsey } end context 'when pipeline status is success' do - let(:pipeline) { create(:ci_pipeline, :success, project: project) } + let(:pipeline) { create(:ci_pipeline, :success) } it 'can generate a codequality report' do expect(subject).to be_truthy @@ -3563,10 +3587,10 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do context 'when pipeline does not have builds with codequality reports' do before do - create(:ci_build, :artifacts, pipeline: pipeline, project: project) + create(:ci_build, :artifacts, pipeline: pipeline) end - let(:pipeline) { create(:ci_pipeline, :success, project: project) } + let(:pipeline) { create(:ci_pipeline, :success) } it { expect(subject).to be_falsey } end @@ -3575,12 +3599,12 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do describe '#test_report_summary' do subject { pipeline.test_report_summary } - context 'when pipeline has multiple builds with report results' do - let(:pipeline) { create(:ci_pipeline, :success, project: project) } + let(:pipeline) { create(:ci_pipeline, :success) } + context 'when pipeline has multiple builds with report results' do before do - create(:ci_build, :success, :report_results, name: 'rspec', pipeline: pipeline, project: project) - create(:ci_build, :success, :report_results, name: 'java', pipeline: pipeline, project: project) + create(:ci_build, :success, :report_results, name: 'rspec', pipeline: pipeline) + create(:ci_build, :success, :report_results, name: 'java', pipeline: pipeline) end it 'returns test report summary with collected data' do @@ -3598,13 +3622,15 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do describe '#test_reports' do subject { pipeline.test_reports } + let_it_be(:pipeline) { create(:ci_pipeline) } + context 'when pipeline has multiple builds with test reports' do - let!(:build_rspec) { create(:ci_build, :success, name: 'rspec', pipeline: pipeline, project: project) } - let!(:build_java) { create(:ci_build, :success, name: 'java', pipeline: pipeline, project: project) } + let!(:build_rspec) { create(:ci_build, :success, name: 'rspec', pipeline: pipeline) } + let!(:build_java) { create(:ci_build, :success, name: 'java', pipeline: pipeline) } before do - create(:ci_job_artifact, :junit, job: build_rspec, project: project) - create(:ci_job_artifact, :junit_with_ant, job: build_java, project: project) + create(:ci_job_artifact, :junit, job: build_rspec) + create(:ci_job_artifact, :junit_with_ant, job: build_java) end it 'returns test reports with collected data' do @@ -3614,8 +3640,8 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end context 'when builds are retried' do - let!(:build_rspec) { create(:ci_build, :retried, :success, name: 'rspec', pipeline: pipeline, project: project) } - let!(:build_java) { create(:ci_build, :retried, :success, name: 'java', pipeline: pipeline, project: project) } + let!(:build_rspec) { create(:ci_build, :retried, :success, name: 'rspec', pipeline: pipeline) } + let!(:build_java) { create(:ci_build, :retried, :success, name: 'java', pipeline: pipeline) } it 'does not take retried builds into account' do expect(subject.total_count).to be(0) @@ -3635,13 +3661,15 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do describe '#accessibility_reports' do subject { pipeline.accessibility_reports } + let_it_be(:pipeline) { create(:ci_pipeline) } + context 'when pipeline has multiple builds with accessibility reports' do - let(:build_rspec) { create(:ci_build, :success, name: 'rspec', pipeline: pipeline, project: project) } - let(:build_golang) { create(:ci_build, :success, name: 'golang', pipeline: pipeline, project: project) } + let(:build_rspec) { create(:ci_build, :success, name: 'rspec', pipeline: pipeline) } + let(:build_golang) { create(:ci_build, :success, name: 'golang', pipeline: pipeline) } before do - create(:ci_job_artifact, :accessibility, job: build_rspec, project: project) - create(:ci_job_artifact, :accessibility_without_errors, job: build_golang, project: project) + create(:ci_job_artifact, :accessibility, job: build_rspec) + create(:ci_job_artifact, :accessibility_without_errors, job: build_golang) end it 'returns accessibility report with collected data' do @@ -3652,8 +3680,8 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end context 'when builds are retried' do - let(:build_rspec) { create(:ci_build, :retried, :success, name: 'rspec', pipeline: pipeline, project: project) } - let(:build_golang) { create(:ci_build, :retried, :success, name: 'golang', pipeline: pipeline, project: project) } + let(:build_rspec) { create(:ci_build, :retried, :success, name: 'rspec', pipeline: pipeline) } + let(:build_golang) { create(:ci_build, :retried, :success, name: 'golang', pipeline: pipeline) } it 'returns empty urls for accessibility reports' do expect(subject.urls).to be_empty @@ -3671,13 +3699,15 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do describe '#coverage_reports' do subject { pipeline.coverage_reports } + let_it_be(:pipeline) { create(:ci_pipeline) } + context 'when pipeline has multiple builds with coverage reports' do - let!(:build_rspec) { create(:ci_build, :success, name: 'rspec', pipeline: pipeline, project: project) } - let!(:build_golang) { create(:ci_build, :success, name: 'golang', pipeline: pipeline, project: project) } + let!(:build_rspec) { create(:ci_build, :success, name: 'rspec', pipeline: pipeline) } + let!(:build_golang) { create(:ci_build, :success, name: 'golang', pipeline: pipeline) } before do - create(:ci_job_artifact, :cobertura, job: build_rspec, project: project) - create(:ci_job_artifact, :coverage_gocov_xml, job: build_golang, project: project) + create(:ci_job_artifact, :cobertura, job: build_rspec) + create(:ci_job_artifact, :coverage_gocov_xml, job: build_golang) end it 'returns coverage reports with collected data' do @@ -3689,8 +3719,8 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end it 'does not execute N+1 queries' do - single_build_pipeline = create(:ci_empty_pipeline, status: :created, project: project) - single_rspec = create(:ci_build, :success, name: 'rspec', pipeline: single_build_pipeline, project: project) + single_build_pipeline = create(:ci_empty_pipeline, :created) + single_rspec = create(:ci_build, :success, name: 'rspec', pipeline: single_build_pipeline) create(:ci_job_artifact, :cobertura, job: single_rspec, project: project) control = ActiveRecord::QueryRecorder.new { single_build_pipeline.coverage_reports } @@ -3699,8 +3729,8 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end context 'when builds are retried' do - let!(:build_rspec) { create(:ci_build, :retried, :success, name: 'rspec', pipeline: pipeline, project: project) } - let!(:build_golang) { create(:ci_build, :retried, :success, name: 'golang', pipeline: pipeline, project: project) } + let!(:build_rspec) { create(:ci_build, :retried, :success, name: 'rspec', pipeline: pipeline) } + let!(:build_golang) { create(:ci_build, :retried, :success, name: 'golang', pipeline: pipeline) } it 'does not take retried builds into account' do expect(subject.files).to eql({}) @@ -3718,13 +3748,15 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do describe '#codequality_reports' do subject(:codequality_reports) { pipeline.codequality_reports } + let_it_be(:pipeline) { create(:ci_pipeline) } + context 'when pipeline has multiple builds with codequality reports' do - let(:build_rspec) { create(:ci_build, :success, name: 'rspec', pipeline: pipeline, project: project) } - let(:build_golang) { create(:ci_build, :success, name: 'golang', pipeline: pipeline, project: project) } + let(:build_rspec) { create(:ci_build, :success, name: 'rspec', pipeline: pipeline) } + let(:build_golang) { create(:ci_build, :success, name: 'golang', pipeline: pipeline) } before do - create(:ci_job_artifact, :codequality, job: build_rspec, project: project) - create(:ci_job_artifact, :codequality_without_errors, job: build_golang, project: project) + create(:ci_job_artifact, :codequality, job: build_rspec) + create(:ci_job_artifact, :codequality_without_errors, job: build_golang) end it 'returns codequality report with collected data' do @@ -3732,8 +3764,8 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end context 'when builds are retried' do - let(:build_rspec) { create(:ci_build, :retried, :success, name: 'rspec', pipeline: pipeline, project: project) } - let(:build_golang) { create(:ci_build, :retried, :success, name: 'golang', pipeline: pipeline, project: project) } + let(:build_rspec) { create(:ci_build, :retried, :success, name: 'rspec', pipeline: pipeline) } + let(:build_golang) { create(:ci_build, :retried, :success, name: 'golang', pipeline: pipeline) } it 'returns a codequality reports without degradations' do expect(codequality_reports.degradations).to be_empty @@ -3749,6 +3781,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end describe '#total_size' do + let(:pipeline) { create(:ci_pipeline) } let!(:build_job1) { create(:ci_build, pipeline: pipeline, stage_idx: 0) } let!(:build_job2) { create(:ci_build, pipeline: pipeline, stage_idx: 0) } let!(:test_job_failed_and_retried) { create(:ci_build, :failed, :retried, pipeline: pipeline, stage_idx: 1) } @@ -3785,17 +3818,11 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end describe '#default_branch?' do - let(:default_branch) { 'master'} - subject { pipeline.default_branch? } - before do - allow(project).to receive(:default_branch).and_return(default_branch) - end - context 'when pipeline ref is the default branch of the project' do let(:pipeline) do - build(:ci_empty_pipeline, status: :created, project: project, ref: default_branch) + build(:ci_empty_pipeline, :created, project: project, ref: project.default_branch) end it "returns true" do @@ -3805,7 +3832,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do context 'when pipeline ref is not the default branch of the project' do let(:pipeline) do - build(:ci_empty_pipeline, status: :created, project: project, ref: 'another_branch') + build(:ci_empty_pipeline, :created, project: project, ref: 'another_branch') end it "returns false" do @@ -3815,7 +3842,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end describe '#find_stage_by_name' do - let(:pipeline) { create(:ci_pipeline) } + let_it_be(:pipeline) { create(:ci_pipeline) } let(:stage_name) { 'test' } let(:stage) do @@ -3895,10 +3922,10 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end describe '#parent_pipeline' do - let_it_be(:project) { create(:project) } + let_it_be_with_reload(:pipeline) { create(:ci_pipeline) } context 'when pipeline is triggered by a pipeline from the same project' do - let_it_be(:upstream_pipeline) { create(:ci_pipeline, project: project) } + let_it_be(:upstream_pipeline) { create(:ci_pipeline) } let_it_be(:pipeline) { create(:ci_pipeline, child_of: upstream_pipeline) } it 'returns the parent pipeline' do @@ -3911,7 +3938,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end context 'when pipeline is triggered by a pipeline from another project' do - let(:pipeline) { create(:ci_pipeline, project: project) } + let(:pipeline) { create(:ci_pipeline) } let!(:upstream_pipeline) { create(:ci_pipeline, project: create(:project), upstream_of: pipeline) } it 'returns nil' do @@ -3938,7 +3965,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do describe '#child_pipelines' do let_it_be(:project) { create(:project) } - let(:pipeline) { create(:ci_pipeline, project: project) } + let_it_be_with_reload(:pipeline) { create(:ci_pipeline, project: project) } context 'when pipeline triggered other pipelines on same project' do let(:downstream_pipeline) { create(:ci_pipeline, project: pipeline.project) } @@ -3992,6 +4019,8 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end describe 'upstream status interactions' do + let_it_be_with_reload(:pipeline) { create(:ci_pipeline, :created) } + context 'when a pipeline has an upstream status' do context 'when an upstream status is a bridge' do let(:bridge) { create(:ci_bridge, status: :pending) } @@ -4050,6 +4079,8 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do describe '#source_ref_path' do subject { pipeline.source_ref_path } + let(:pipeline) { create(:ci_pipeline, :created) } + context 'when pipeline is for a branch' do it { is_expected.to eq(Gitlab::Git::BRANCH_REF_PREFIX + pipeline.source_ref.to_s) } end @@ -4062,13 +4093,15 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end context 'when pipeline is for a tag' do - let(:pipeline) { create(:ci_pipeline, project: project, tag: true) } + let(:pipeline) { create(:ci_pipeline, tag: true) } it { is_expected.to eq(Gitlab::Git::TAG_REF_PREFIX + pipeline.source_ref.to_s) } end end - describe "#builds_with_coverage" do + describe '#builds_with_coverage' do + let_it_be(:pipeline) { create(:ci_pipeline, :created) } + it 'returns builds with coverage only' do rspec = create(:ci_build, name: 'rspec', coverage: 97.1, pipeline: pipeline) jest = create(:ci_build, name: 'jest', coverage: 94.1, pipeline: pipeline) @@ -4092,10 +4125,11 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end describe '#base_and_ancestors' do - let(:same_project) { false } - subject { pipeline.base_and_ancestors(same_project: same_project) } + let_it_be(:pipeline) { create(:ci_pipeline, :created) } + let(:same_project) { false } + context 'when pipeline is not child nor parent' do it 'returns just the pipeline itself' do expect(subject).to contain_exactly(pipeline) @@ -4103,8 +4137,8 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end context 'when pipeline is child' do - let(:parent) { create(:ci_pipeline, project: pipeline.project) } - let(:sibling) { create(:ci_pipeline, project: pipeline.project) } + let(:parent) { create(:ci_pipeline) } + let(:sibling) { create(:ci_pipeline) } before do create_source_pipeline(parent, pipeline) @@ -4117,7 +4151,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end context 'when pipeline is parent' do - let(:child) { create(:ci_pipeline, project: pipeline.project) } + let(:child) { create(:ci_pipeline) } before do create_source_pipeline(pipeline, child) @@ -4129,8 +4163,9 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end context 'when pipeline is a child of a child pipeline' do - let(:ancestor) { create(:ci_pipeline, project: pipeline.project) } - let(:parent) { create(:ci_pipeline, project: pipeline.project) } + let_it_be(:pipeline) { create(:ci_pipeline, :created) } + let(:ancestor) { create(:ci_pipeline) } + let(:parent) { create(:ci_pipeline) } before do create_source_pipeline(ancestor, parent) @@ -4143,6 +4178,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end context 'when pipeline is a triggered pipeline' do + let_it_be(:pipeline) { create(:ci_pipeline, :created) } let(:upstream) { create(:ci_pipeline, project: create(:project)) } before do @@ -4166,8 +4202,10 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end describe 'reset_ancestor_bridges!' do + let_it_be(:pipeline) { create(:ci_pipeline, :created) } + context 'when the pipeline is a child pipeline and the bridge is depended' do - let!(:parent_pipeline) { create(:ci_pipeline, project: project) } + let!(:parent_pipeline) { create(:ci_pipeline) } let!(:bridge) { create_bridge(parent_pipeline, pipeline, true) } it 'marks source bridge as pending' do @@ -4191,7 +4229,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end context 'when the pipeline is a child pipeline and the bridge is not depended' do - let!(:parent_pipeline) { create(:ci_pipeline, project: project) } + let!(:parent_pipeline) { create(:ci_pipeline) } let!(:bridge) { create_bridge(parent_pipeline, pipeline, false) } it 'does not touch source bridge' do @@ -4227,6 +4265,8 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end describe 'test failure history processing' do + let(:pipeline) { build(:ci_pipeline, :created) } + it 'performs the service asynchronously when the pipeline is completed' do service = double @@ -4238,21 +4278,23 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end describe '#latest_test_report_builds' do + let_it_be(:pipeline) { create(:ci_pipeline, :created) } + it 'returns pipeline builds with test report artifacts' do - test_build = create(:ci_build, :test_reports, pipeline: pipeline, project: project) + test_build = create(:ci_build, :test_reports, pipeline: pipeline) create(:ci_build, :artifacts, pipeline: pipeline, project: project) expect(pipeline.latest_test_report_builds).to contain_exactly(test_build) end it 'preloads project on each build to avoid N+1 queries' do - create(:ci_build, :test_reports, pipeline: pipeline, project: project) + create(:ci_build, :test_reports, pipeline: pipeline) control_count = ActiveRecord::QueryRecorder.new do pipeline.latest_test_report_builds.map(&:project).map(&:full_path) end - multi_build_pipeline = create(:ci_empty_pipeline, status: :created, project: project) + multi_build_pipeline = create(:ci_empty_pipeline, :created) create(:ci_build, :test_reports, pipeline: multi_build_pipeline, project: project) create(:ci_build, :test_reports, pipeline: multi_build_pipeline, project: project) @@ -4262,30 +4304,32 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end describe '#builds_with_failed_tests' do + let_it_be(:pipeline) { create(:ci_pipeline, :created) } + it 'returns pipeline builds with test report artifacts' do - failed_build = create(:ci_build, :failed, :test_reports, pipeline: pipeline, project: project) - create(:ci_build, :success, :test_reports, pipeline: pipeline, project: project) + failed_build = create(:ci_build, :failed, :test_reports, pipeline: pipeline) + create(:ci_build, :success, :test_reports, pipeline: pipeline) expect(pipeline.builds_with_failed_tests).to contain_exactly(failed_build) end it 'supports limiting the number of builds to fetch' do - create(:ci_build, :failed, :test_reports, pipeline: pipeline, project: project) - create(:ci_build, :failed, :test_reports, pipeline: pipeline, project: project) + create(:ci_build, :failed, :test_reports, pipeline: pipeline) + create(:ci_build, :failed, :test_reports, pipeline: pipeline) expect(pipeline.builds_with_failed_tests(limit: 1).count).to eq(1) end it 'preloads project on each build to avoid N+1 queries' do - create(:ci_build, :failed, :test_reports, pipeline: pipeline, project: project) + create(:ci_build, :failed, :test_reports, pipeline: pipeline) control_count = ActiveRecord::QueryRecorder.new do pipeline.builds_with_failed_tests.map(&:project).map(&:full_path) end - multi_build_pipeline = create(:ci_empty_pipeline, status: :created, project: project) - create(:ci_build, :failed, :test_reports, pipeline: multi_build_pipeline, project: project) - create(:ci_build, :failed, :test_reports, pipeline: multi_build_pipeline, project: project) + multi_build_pipeline = create(:ci_empty_pipeline, :created) + create(:ci_build, :failed, :test_reports, pipeline: multi_build_pipeline) + create(:ci_build, :failed, :test_reports, pipeline: multi_build_pipeline) expect { multi_build_pipeline.builds_with_failed_tests.map(&:project).map(&:full_path) } .not_to exceed_query_limit(control_count) diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb index 3e5d068d780..ff3551d2a18 100644 --- a/spec/models/ci/runner_spec.rb +++ b/spec/models/ci/runner_spec.rb @@ -40,41 +40,39 @@ RSpec.describe Ci::Runner do context 'runner_type validations' do let_it_be(:group) { create(:group) } let_it_be(:project) { create(:project) } - let(:group_runner) { create(:ci_runner, :group, groups: [group]) } - let(:project_runner) { create(:ci_runner, :project, projects: [project]) } - let(:instance_runner) { create(:ci_runner, :instance) } it 'disallows assigning group to project_type runner' do - project_runner.groups << build(:group) + project_runner = build(:ci_runner, :project, groups: [group]) expect(project_runner).not_to be_valid expect(project_runner.errors.full_messages).to include('Runner cannot have groups assigned') end it 'disallows assigning group to instance_type runner' do - instance_runner.groups << build(:group) + instance_runner = build(:ci_runner, :instance, groups: [group]) expect(instance_runner).not_to be_valid expect(instance_runner.errors.full_messages).to include('Runner cannot have groups assigned') end it 'disallows assigning project to group_type runner' do - group_runner.projects << build(:project) + group_runner = build(:ci_runner, :instance, projects: [project]) expect(group_runner).not_to be_valid expect(group_runner.errors.full_messages).to include('Runner cannot have projects assigned') end it 'disallows assigning project to instance_type runner' do - instance_runner.projects << build(:project) + instance_runner = build(:ci_runner, :instance, projects: [project]) expect(instance_runner).not_to be_valid expect(instance_runner.errors.full_messages).to include('Runner cannot have projects assigned') end it 'fails to save a group assigned to a project runner even if the runner is already saved' do - group.runners << project_runner - expect { group.save! } + project_runner = create(:ci_runner, :project, projects: [project]) + + expect { create(:group, runners: [project_runner]) } .to raise_error(ActiveRecord::RecordInvalid) end end @@ -352,6 +350,8 @@ RSpec.describe Ci::Runner do end describe '#can_pick?' do + using RSpec::Parameterized::TableSyntax + let_it_be(:pipeline) { create(:ci_pipeline) } let(:build) { create(:ci_build, pipeline: pipeline) } let(:runner_project) { build.project } @@ -365,6 +365,11 @@ RSpec.describe Ci::Runner do let(:other_project) { create(:project) } let(:other_runner) { create(:ci_runner, :project, projects: [other_project], tag_list: tag_list, run_untagged: run_untagged) } + before do + # `can_pick?` is not used outside the runners available for the project + stub_feature_flags(ci_runners_short_circuit_assignable_for: false) + end + it 'cannot handle builds' do expect(other_runner.can_pick?(build)).to be_falsey end @@ -432,9 +437,32 @@ RSpec.describe Ci::Runner do expect(runner.can_pick?(build)).to be_truthy end end + + it 'does not query for owned or instance runners' do + expect(described_class).not_to receive(:owned_or_instance_wide) + + runner.can_pick?(build) + end + + context 'when feature flag ci_runners_short_circuit_assignable_for is disabled' do + before do + stub_feature_flags(ci_runners_short_circuit_assignable_for: false) + end + + it 'does not query for owned or instance runners' do + expect(described_class).to receive(:owned_or_instance_wide).and_call_original + + runner.can_pick?(build) + end + end end context 'when runner is not shared' do + before do + # `can_pick?` is not used outside the runners available for the project + stub_feature_flags(ci_runners_short_circuit_assignable_for: false) + end + context 'when runner is assigned to a project' do it 'can handle builds' do expect(runner.can_pick?(build)).to be_truthy @@ -502,6 +530,29 @@ RSpec.describe Ci::Runner do it { is_expected.to be_falsey } end end + + context 'matches tags' do + where(:run_untagged, :runner_tags, :build_tags, :result) do + true | [] | [] | true + true | [] | ['a'] | false + true | %w[a b] | ['a'] | true + true | ['a'] | %w[a b] | false + true | ['a'] | ['a'] | true + false | ['a'] | ['a'] | true + false | ['b'] | ['a'] | false + false | %w[a b] | ['a'] | true + end + + with_them do + let(:tag_list) { runner_tags } + + before do + build.tag_list = build_tags + end + + it { is_expected.to eq(result) } + end + end end describe '#status' do @@ -844,27 +895,50 @@ RSpec.describe Ci::Runner do end describe '#pick_build!' do + let(:build) { create(:ci_build) } + let(:runner) { create(:ci_runner) } + context 'runner can pick the build' do it 'calls #tick_runner_queue' do - ci_build = build(:ci_build) - runner = build(:ci_runner) - allow(runner).to receive(:can_pick?).with(ci_build).and_return(true) - expect(runner).to receive(:tick_runner_queue) - runner.pick_build!(ci_build) + runner.pick_build!(build) end end context 'runner cannot pick the build' do - it 'does not call #tick_runner_queue' do - ci_build = build(:ci_build) - runner = build(:ci_runner) - allow(runner).to receive(:can_pick?).with(ci_build).and_return(false) + before do + build.tag_list = [:docker] + end + it 'does not call #tick_runner_queue' do expect(runner).not_to receive(:tick_runner_queue) - runner.pick_build!(ci_build) + runner.pick_build!(build) + end + end + + context 'build picking improvement enabled' do + before do + stub_feature_flags(ci_reduce_queries_when_ticking_runner_queue: true) + end + + it 'does not check if the build is assignable to a runner' do + expect(runner).not_to receive(:can_pick?) + + runner.pick_build!(build) + end + end + + context 'build picking improvement disabled' do + before do + stub_feature_flags(ci_reduce_queries_when_ticking_runner_queue: false) + end + + it 'checks if the build is assignable to a runner' do + expect(runner).to receive(:can_pick?).and_call_original + + runner.pick_build!(build) end end end diff --git a/spec/models/ci/variable_spec.rb b/spec/models/ci/variable_spec.rb index 26a7a2596af..93a24ba9157 100644 --- a/spec/models/ci/variable_spec.rb +++ b/spec/models/ci/variable_spec.rb @@ -14,6 +14,15 @@ RSpec.describe Ci::Variable do it { is_expected.to validate_uniqueness_of(:key).scoped_to(:project_id, :environment_scope).with_message(/\(\w+\) has already been taken/) } end + describe '.by_environment_scope' do + let!(:matching_variable) { create(:ci_variable, environment_scope: 'production ') } + let!(:non_matching_variable) { create(:ci_variable, environment_scope: 'staging') } + + subject { Ci::Variable.by_environment_scope('production') } + + it { is_expected.to contain_exactly(matching_variable) } + end + describe '.unprotected' do subject { described_class.unprotected } diff --git a/spec/models/clusters/agent_token_spec.rb b/spec/models/clusters/agent_token_spec.rb index 5cb84ee131a..a1b45df1970 100644 --- a/spec/models/clusters/agent_token_spec.rb +++ b/spec/models/clusters/agent_token_spec.rb @@ -3,8 +3,11 @@ require 'spec_helper' RSpec.describe Clusters::AgentToken do - it { is_expected.to belong_to(:agent).class_name('Clusters::Agent') } + it { is_expected.to belong_to(:agent).class_name('Clusters::Agent').required } it { is_expected.to belong_to(:created_by_user).class_name('User').optional } + it { is_expected.to validate_length_of(:description).is_at_most(1024) } + it { is_expected.to validate_length_of(:name).is_at_most(255) } + it { is_expected.to validate_presence_of(:name) } describe '#token' do it 'is generated on save' do diff --git a/spec/models/concerns/ci/has_variable_spec.rb b/spec/models/concerns/ci/has_variable_spec.rb index b5390281064..e917ec6b802 100644 --- a/spec/models/concerns/ci/has_variable_spec.rb +++ b/spec/models/concerns/ci/has_variable_spec.rb @@ -11,6 +11,17 @@ RSpec.describe Ci::HasVariable do it { is_expected.not_to allow_value('foo bar').for(:key) } it { is_expected.not_to allow_value('foo/bar').for(:key) } + describe 'scopes' do + describe '.by_key' do + let!(:matching_variable) { create(:ci_variable, key: 'example') } + let!(:non_matching_variable) { create(:ci_variable, key: 'other') } + + subject { Ci::Variable.by_key('example') } + + it { is_expected.to contain_exactly(matching_variable) } + end + end + describe '#key=' do context 'when the new key is nil' do it 'strips leading and trailing whitespaces' do diff --git a/spec/models/concerns/project_features_compatibility_spec.rb b/spec/models/concerns/project_features_compatibility_spec.rb index 2059e170446..62c9a041a85 100644 --- a/spec/models/concerns/project_features_compatibility_spec.rb +++ b/spec/models/concerns/project_features_compatibility_spec.rb @@ -4,8 +4,8 @@ require 'spec_helper' RSpec.describe ProjectFeaturesCompatibility do let(:project) { create(:project) } - let(:features_enabled) { %w(issues wiki builds merge_requests snippets) } - let(:features) { features_enabled + %w(repository pages operations) } + let(:features_enabled) { %w(issues wiki builds merge_requests snippets security_and_compliance) } + let(:features) { features_enabled + %w(repository pages operations container_registry) } # We had issues_enabled, snippets_enabled, builds_enabled, merge_requests_enabled and issues_enabled fields on projects table # All those fields got moved to a new table called project_feature and are now integers instead of booleans diff --git a/spec/models/custom_emoji_spec.rb b/spec/models/custom_emoji_spec.rb index 41ce480b02f..e34934d393a 100644 --- a/spec/models/custom_emoji_spec.rb +++ b/spec/models/custom_emoji_spec.rb @@ -4,8 +4,10 @@ require 'spec_helper' RSpec.describe CustomEmoji do describe 'Associations' do - it { is_expected.to belong_to(:namespace) } + it { is_expected.to belong_to(:namespace).inverse_of(:custom_emoji) } + it { is_expected.to belong_to(:creator).inverse_of(:created_custom_emoji) } it { is_expected.to have_db_column(:file) } + it { is_expected.to validate_presence_of(:creator) } it { is_expected.to validate_length_of(:name).is_at_most(36) } it { is_expected.to validate_presence_of(:name) } it { is_expected.to have_db_column(:external) } @@ -36,7 +38,7 @@ RSpec.describe CustomEmoji do new_emoji = build(:custom_emoji, name: old_emoji.name, namespace: old_emoji.namespace, group: group) expect(new_emoji).not_to be_valid - expect(new_emoji.errors.messages).to eq(name: ["has already been taken"]) + expect(new_emoji.errors.messages).to include(name: ["has already been taken"]) end it 'disallows non http and https file value' do diff --git a/spec/models/dependency_proxy/manifest_spec.rb b/spec/models/dependency_proxy/manifest_spec.rb index aa2e73356dd..4203644c003 100644 --- a/spec/models/dependency_proxy/manifest_spec.rb +++ b/spec/models/dependency_proxy/manifest_spec.rb @@ -29,24 +29,32 @@ RSpec.describe DependencyProxy::Manifest, type: :model do end end - describe '.find_or_initialize_by_file_name' do - subject { DependencyProxy::Manifest.find_or_initialize_by_file_name(file_name) } + describe '.find_or_initialize_by_file_name_or_digest' do + let_it_be(:file_name) { 'foo' } + let_it_be(:digest) { 'bar' } - context 'no manifest exists' do - let_it_be(:file_name) { 'foo' } + subject { DependencyProxy::Manifest.find_or_initialize_by_file_name_or_digest(file_name: file_name, digest: digest) } + context 'no manifest exists' do it 'initializes a manifest' do - expect(DependencyProxy::Manifest).to receive(:new).with(file_name: file_name) + expect(DependencyProxy::Manifest).to receive(:new).with(file_name: file_name, digest: digest) subject end end - context 'manifest exists' do + context 'manifest exists and matches file_name' do let_it_be(:dependency_proxy_manifest) { create(:dependency_proxy_manifest) } let_it_be(:file_name) { dependency_proxy_manifest.file_name } it { is_expected.to eq(dependency_proxy_manifest) } end + + context 'manifest exists and matches digest' do + let_it_be(:dependency_proxy_manifest) { create(:dependency_proxy_manifest) } + let_it_be(:digest) { dependency_proxy_manifest.digest } + + it { is_expected.to eq(dependency_proxy_manifest) } + end end end diff --git a/spec/models/email_spec.rb b/spec/models/email_spec.rb index ffdc621dd4c..62f2a53ab3c 100644 --- a/spec/models/email_spec.rb +++ b/spec/models/email_spec.rb @@ -10,7 +10,7 @@ RSpec.describe Email do end describe 'validations' do - it_behaves_like 'an object with RFC3696 compliant email-formated attributes', :email do + it_behaves_like 'an object with RFC3696 compliant email-formatted attributes', :email do subject { build(:email) } end end diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb index 0c7d8e2969d..30eb752d2d5 100644 --- a/spec/models/environment_spec.rb +++ b/spec/models/environment_spec.rb @@ -34,6 +34,39 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do it { is_expected.to validate_length_of(:external_url).is_at_most(255) } + describe '.before_save' do + it 'ensures environment tier when a new object is created' do + environment = build(:environment, name: 'gprd', tier: nil) + + expect { environment.save }.to change { environment.tier }.from(nil).to('production') + end + + it 'ensures environment tier when an existing object is updated' do + environment = create(:environment, name: 'gprd') + environment.update_column(:tier, nil) + + expect { environment.stop! }.to change { environment.reload.tier }.from(nil).to('production') + end + + it 'does not overwrite the existing environment tier' do + environment = create(:environment, name: 'gprd', tier: :production) + + expect { environment.update!(name: 'gstg') }.not_to change { environment.reload.tier } + end + + context 'when environment_tier feature flag is disabled' do + before do + stub_feature_flags(environment_tier: false) + end + + it 'does not ensure environment tier' do + environment = build(:environment, name: 'gprd', tier: nil) + + expect { environment.save }.not_to change { environment.tier } + end + end + end + describe '.order_by_last_deployed_at' do let!(:environment1) { create(:environment, project: project) } let!(:environment2) { create(:environment, project: project) } @@ -195,6 +228,62 @@ RSpec.describe Environment, :use_clean_rails_memory_store_caching do end end + describe '.for_tier' do + let_it_be(:environment) { create(:environment, :production) } + + it 'returns the production environment when searching for production tier' do + expect(described_class.for_tier(:production)).to eq([environment]) + end + + it 'returns nothing when searching for staging tier' do + expect(described_class.for_tier(:staging)).to be_empty + end + end + + describe '#guess_tier' do + using RSpec::Parameterized::TableSyntax + + subject { environment.send(:guess_tier) } + + let(:environment) { build(:environment, name: name) } + + where(:name, :tier) do + 'review/feature' | described_class.tiers[:development] + 'review/product' | described_class.tiers[:development] + 'DEV' | described_class.tiers[:development] + 'development' | described_class.tiers[:development] + 'trunk' | described_class.tiers[:development] + 'test' | described_class.tiers[:testing] + 'TEST' | described_class.tiers[:testing] + 'testing' | described_class.tiers[:testing] + 'testing-prd' | described_class.tiers[:testing] + 'acceptance-testing' | described_class.tiers[:testing] + 'QC' | described_class.tiers[:testing] + 'gstg' | described_class.tiers[:staging] + 'staging' | described_class.tiers[:staging] + 'stage' | described_class.tiers[:staging] + 'Model' | described_class.tiers[:staging] + 'MODL' | described_class.tiers[:staging] + 'Pre-production' | described_class.tiers[:staging] + 'pre' | described_class.tiers[:staging] + 'Demo' | described_class.tiers[:staging] + 'gprd' | described_class.tiers[:production] + 'gprd-cny' | described_class.tiers[:production] + 'production' | described_class.tiers[:production] + 'Production' | described_class.tiers[:production] + 'prod' | described_class.tiers[:production] + 'PROD' | described_class.tiers[:production] + 'Live' | described_class.tiers[:production] + 'canary' | described_class.tiers[:other] + 'other' | described_class.tiers[:other] + 'EXP' | described_class.tiers[:other] + end + + with_them do + it { is_expected.to eq(tier) } + end + end + describe '#expire_etag_cache' do let(:store) { Gitlab::EtagCaching::Store.new } diff --git a/spec/models/error_tracking/project_error_tracking_setting_spec.rb b/spec/models/error_tracking/project_error_tracking_setting_spec.rb index 72ed11f6c74..3ae0666f7d0 100644 --- a/spec/models/error_tracking/project_error_tracking_setting_spec.rb +++ b/spec/models/error_tracking/project_error_tracking_setting_spec.rb @@ -111,7 +111,7 @@ RSpec.describe ErrorTracking::ProjectErrorTrackingSetting do describe '#sentry_client' do it 'returns sentry client' do - expect(subject.sentry_client).to be_a(Sentry::Client) + expect(subject.sentry_client).to be_a(ErrorTracking::SentryClient) end end @@ -152,7 +152,7 @@ RSpec.describe ErrorTracking::ProjectErrorTrackingSetting do end end - context 'when sentry client raises Sentry::Client::Error' do + context 'when sentry client raises ErrorTracking::SentryClient::Error' do let(:sentry_client) { spy(:sentry_client) } before do @@ -160,7 +160,7 @@ RSpec.describe ErrorTracking::ProjectErrorTrackingSetting do allow(subject).to receive(:sentry_client).and_return(sentry_client) allow(sentry_client).to receive(:list_issues).with(opts) - .and_raise(Sentry::Client::Error, 'error message') + .and_raise(ErrorTracking::SentryClient::Error, 'error message') end it 'returns error' do @@ -171,7 +171,7 @@ RSpec.describe ErrorTracking::ProjectErrorTrackingSetting do end end - context 'when sentry client raises Sentry::Client::MissingKeysError' do + context 'when sentry client raises ErrorTracking::SentryClient::MissingKeysError' do let(:sentry_client) { spy(:sentry_client) } before do @@ -179,7 +179,7 @@ RSpec.describe ErrorTracking::ProjectErrorTrackingSetting do allow(subject).to receive(:sentry_client).and_return(sentry_client) allow(sentry_client).to receive(:list_issues).with(opts) - .and_raise(Sentry::Client::MissingKeysError, 'Sentry API response is missing keys. key not found: "id"') + .and_raise(ErrorTracking::SentryClient::MissingKeysError, 'Sentry API response is missing keys. key not found: "id"') end it 'returns error' do @@ -190,7 +190,7 @@ RSpec.describe ErrorTracking::ProjectErrorTrackingSetting do end end - context 'when sentry client raises Sentry::Client::ResponseInvalidSizeError' do + context 'when sentry client raises ErrorTracking::SentryClient::ResponseInvalidSizeError' do let(:sentry_client) { spy(:sentry_client) } let(:error_msg) {"Sentry API response is too big. Limit is #{Gitlab::Utils::DeepSize.human_default_max_size}."} @@ -199,7 +199,7 @@ RSpec.describe ErrorTracking::ProjectErrorTrackingSetting do allow(subject).to receive(:sentry_client).and_return(sentry_client) allow(sentry_client).to receive(:list_issues).with(opts) - .and_raise(Sentry::Client::ResponseInvalidSizeError, error_msg) + .and_raise(ErrorTracking::SentryClient::ResponseInvalidSizeError, error_msg) end it 'returns error' do diff --git a/spec/models/experiment_spec.rb b/spec/models/experiment_spec.rb index 22bbf2df8fd..09dd1766acc 100644 --- a/spec/models/experiment_spec.rb +++ b/spec/models/experiment_spec.rb @@ -98,10 +98,11 @@ RSpec.describe Experiment do describe '.record_conversion_event' do let_it_be(:user) { build(:user) } + let_it_be(:context) { { a: 42 } } let(:experiment_key) { :test_experiment } - subject(:record_conversion_event) { described_class.record_conversion_event(experiment_key, user) } + subject(:record_conversion_event) { described_class.record_conversion_event(experiment_key, user, context) } context 'when no matching experiment exists' do it 'creates the experiment and uses it' do @@ -127,22 +128,79 @@ RSpec.describe Experiment do it 'sends record_conversion_event_for_user to the experiment instance' do expect_next_found_instance_of(described_class) do |experiment| - expect(experiment).to receive(:record_conversion_event_for_user).with(user) + expect(experiment).to receive(:record_conversion_event_for_user).with(user, context) end record_conversion_event end end end + shared_examples 'experiment user with context' do + let_it_be(:context) { { a: 42, 'b' => 34, 'c': { c1: 100, c2: 'c2', e: :e }, d: [1, 3] } } + let_it_be(:initial_expected_context) { { 'a' => 42, 'b' => 34, 'c' => { 'c1' => 100, 'c2' => 'c2', 'e' => 'e' }, 'd' => [1, 3] } } + + before do + subject + experiment.record_user_and_group(user, :experimental, {}) + end + + it 'has an initial context with stringified keys' do + expect(ExperimentUser.last.context).to eq(initial_expected_context) + end + + context 'when updated' do + before do + subject + experiment.record_user_and_group(user, :experimental, new_context) + end + + context 'with an empty context' do + let_it_be(:new_context) { {} } + + it 'keeps the initial context' do + expect(ExperimentUser.last.context).to eq(initial_expected_context) + end + end + + context 'with string keys' do + let_it_be(:new_context) { { f: :some_symbol } } + + it 'adds new symbols stringified' do + expected_context = initial_expected_context.merge('f' => 'some_symbol') + expect(ExperimentUser.last.context).to eq(expected_context) + end + end + + context 'with atomic values or array values' do + let_it_be(:new_context) { { b: 97, d: [99] } } + + it 'overrides the values' do + expected_context = { 'a' => 42, 'b' => 97, 'c' => { 'c1' => 100, 'c2' => 'c2', 'e' => 'e' }, 'd' => [99] } + expect(ExperimentUser.last.context).to eq(expected_context) + end + end + + context 'with nested hashes' do + let_it_be(:new_context) { { c: { g: 107 } } } + + it 'inserts nested additional values in the same keys' do + expected_context = initial_expected_context.deep_merge('c' => { 'g' => 107 }) + expect(ExperimentUser.last.context).to eq(expected_context) + end + end + end + end + describe '#record_conversion_event_for_user' do let_it_be(:user) { create(:user) } let_it_be(:experiment) { create(:experiment) } + let_it_be(:context) { { a: 42 } } - subject(:record_conversion_event_for_user) { experiment.record_conversion_event_for_user(user) } + subject { experiment.record_conversion_event_for_user(user, context) } context 'when no existing experiment_user record exists for the given user' do it 'does not update or create an experiment_user record' do - expect { record_conversion_event_for_user }.not_to change { ExperimentUser.all.to_a } + expect { subject }.not_to change { ExperimentUser.all.to_a } end end @@ -151,7 +209,13 @@ RSpec.describe Experiment do let!(:experiment_user) { create(:experiment_user, experiment: experiment, user: user, converted_at: 2.days.ago) } it 'does not update the converted_at value' do - expect { record_conversion_event_for_user }.not_to change { experiment_user.converted_at } + expect { subject }.not_to change { experiment_user.converted_at } + end + + it_behaves_like 'experiment user with context' do + before do + experiment.record_user_and_group(user, :experimental, context) + end end end @@ -159,7 +223,13 @@ RSpec.describe Experiment do let(:experiment_user) { create(:experiment_user, experiment: experiment, user: user) } it 'updates the converted_at value' do - expect { record_conversion_event_for_user }.to change { experiment_user.reload.converted_at } + expect { subject }.to change { experiment_user.reload.converted_at } + end + + it_behaves_like 'experiment user with context' do + before do + experiment.record_user_and_group(user, :experimental, context) + end end end end @@ -196,24 +266,25 @@ RSpec.describe Experiment do describe '#record_user_and_group' do let_it_be(:experiment) { create(:experiment) } let_it_be(:user) { create(:user) } + let_it_be(:group) { :control } + let_it_be(:context) { { a: 42 } } - let(:group) { :control } - let(:context) { { a: 42 } } - - subject(:record_user_and_group) { experiment.record_user_and_group(user, group, context) } + subject { experiment.record_user_and_group(user, group, context) } context 'when an experiment_user does not yet exist for the given user' do it 'creates a new experiment_user record' do - expect { record_user_and_group }.to change(ExperimentUser, :count).by(1) + expect { subject }.to change(ExperimentUser, :count).by(1) end it 'assigns the correct group_type to the experiment_user' do - record_user_and_group + subject + expect(ExperimentUser.last.group_type).to eq('control') end it 'adds the correct context to the experiment_user' do - record_user_and_group + subject + expect(ExperimentUser.last.context).to eq({ 'a' => 42 }) end end @@ -225,72 +296,18 @@ RSpec.describe Experiment do end it 'does not create a new experiment_user record' do - expect { record_user_and_group }.not_to change(ExperimentUser, :count) + expect { subject }.not_to change(ExperimentUser, :count) end context 'but the group_type and context has changed' do let(:group) { :experimental } it 'updates the existing experiment_user record with group_type' do - expect { record_user_and_group }.to change { ExperimentUser.last.group_type } + expect { subject }.to change { ExperimentUser.last.group_type } end end - end - - context 'when a context already exists' do - let_it_be(:context) { { a: 42, 'b' => 34, 'c': { c1: 100, c2: 'c2', e: :e }, d: [1, 3] } } - let_it_be(:initial_expected_context) { { 'a' => 42, 'b' => 34, 'c' => { 'c1' => 100, 'c2' => 'c2', 'e' => 'e' }, 'd' => [1, 3] } } - - before do - record_user_and_group - experiment.record_user_and_group(user, :control, {}) - end - - it 'has an initial context with stringified keys' do - expect(ExperimentUser.last.context).to eq(initial_expected_context) - end - - context 'when updated' do - before do - record_user_and_group - experiment.record_user_and_group(user, :control, new_context) - end - - context 'with an empty context' do - let_it_be(:new_context) { {} } - it 'keeps the initial context' do - expect(ExperimentUser.last.context).to eq(initial_expected_context) - end - end - - context 'with string keys' do - let_it_be(:new_context) { { f: :some_symbol } } - - it 'adds new symbols stringified' do - expected_context = initial_expected_context.merge('f' => 'some_symbol') - expect(ExperimentUser.last.context).to eq(expected_context) - end - end - - context 'with atomic values or array values' do - let_it_be(:new_context) { { b: 97, d: [99] } } - - it 'overrides the values' do - expected_context = { 'a' => 42, 'b' => 97, 'c' => { 'c1' => 100, 'c2' => 'c2', 'e' => 'e' }, 'd' => [99] } - expect(ExperimentUser.last.context).to eq(expected_context) - end - end - - context 'with nested hashes' do - let_it_be(:new_context) { { c: { g: 107 } } } - - it 'inserts nested additional values in the same keys' do - expected_context = initial_expected_context.deep_merge('c' => { 'g' => 107 }) - expect(ExperimentUser.last.context).to eq(expected_context) - end - end - end + it_behaves_like 'experiment user with context' end end end diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index e79b54b4674..20983744919 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -25,7 +25,6 @@ RSpec.describe Group do it { is_expected.to have_many(:clusters).class_name('Clusters::Cluster') } it { is_expected.to have_many(:container_repositories) } it { is_expected.to have_many(:milestones) } - it { is_expected.to have_many(:iterations) } it { is_expected.to have_many(:group_deploy_keys) } it { is_expected.to have_many(:services) } it { is_expected.to have_one(:dependency_proxy_setting) } @@ -65,6 +64,59 @@ RSpec.describe Group do it { is_expected.to validate_presence_of :two_factor_grace_period } it { is_expected.to validate_numericality_of(:two_factor_grace_period).is_greater_than_or_equal_to(0) } + context 'validating the parent of a group' do + context 'when the group has no parent' do + it 'allows a group to have no parent associated with it' do + group = build(:group) + + expect(group).to be_valid + end + end + + context 'when the group has a parent' do + it 'does not allow a group to have a namespace as its parent' do + group = build(:group, parent: build(:namespace)) + + expect(group).not_to be_valid + expect(group.errors[:parent_id].first).to eq('a group cannot have a user namespace as its parent') + end + + it 'allows a group to have another group as its parent' do + group = build(:group, parent: build(:group)) + + expect(group).to be_valid + end + end + + context 'when the feature flag `validate_namespace_parent_type` is disabled' do + before do + stub_feature_flags(validate_namespace_parent_type: false) + end + + context 'when the group has no parent' do + it 'allows a group to have no parent associated with it' do + group = build(:group) + + expect(group).to be_valid + end + end + + context 'when the group has a parent' do + it 'allows a group to have a namespace as its parent' do + group = build(:group, parent: build(:namespace)) + + expect(group).to be_valid + end + + it 'allows a group to have another group as its parent' do + group = build(:group, parent: build(:group)) + + expect(group).to be_valid + end + end + end + end + describe 'path validation' do it 'rejects paths reserved on the root namespace when the group has no parent' do group = build(:group, path: 'api') @@ -729,8 +781,16 @@ RSpec.describe Group do context 'evaluating admin access level' do let_it_be(:admin) { create(:admin) } - it 'returns OWNER by default' do - expect(group.max_member_access_for_user(admin)).to eq(Gitlab::Access::OWNER) + context 'when admin mode is enabled', :enable_admin_mode do + it 'returns OWNER by default' do + expect(group.max_member_access_for_user(admin)).to eq(Gitlab::Access::OWNER) + end + end + + context 'when admin mode is disabled' do + it 'returns NO_ACCESS' do + expect(group.max_member_access_for_user(admin)).to eq(Gitlab::Access::NO_ACCESS) + end end it 'returns NO_ACCESS when only concrete membership should be considered' do @@ -740,6 +800,33 @@ RSpec.describe Group do end end + describe '#direct_members' do + let_it_be(:group) { create(:group, :nested) } + let_it_be(:maintainer) { group.parent.add_user(create(:user), GroupMember::MAINTAINER) } + let_it_be(:developer) { group.add_user(create(:user), GroupMember::DEVELOPER) } + + it 'does not return members of the parent' do + expect(group.direct_members).not_to include(maintainer) + end + + it 'returns the direct member of the group' do + expect(group.direct_members).to include(developer) + end + + context 'group sharing' do + let!(:shared_group) { create(:group) } + + before do + create(:group_group_link, shared_group: shared_group, shared_with_group: group) + end + + it 'does not return members of the shared_with group' do + expect(shared_group.direct_members).not_to( + include(developer)) + end + end + end + describe '#members_with_parents' do let!(:group) { create(:group, :nested) } let!(:maintainer) { group.parent.add_user(create(:user), GroupMember::MAINTAINER) } @@ -932,6 +1019,65 @@ RSpec.describe Group do end end + describe '#refresh_members_authorized_projects' do + let_it_be(:group) { create(:group, :nested) } + let_it_be(:parent_group_user) { create(:user) } + let_it_be(:group_user) { create(:user) } + + before do + group.parent.add_maintainer(parent_group_user) + group.add_developer(group_user) + end + + context 'users for which authorizations refresh is executed' do + it 'processes authorizations refresh for all members of the group' do + expect(UserProjectAccessChangedService).to receive(:new).with(contain_exactly(group_user.id, parent_group_user.id)).and_call_original + + group.refresh_members_authorized_projects + end + + context 'when explicitly specified to run only for direct members' do + it 'processes authorizations refresh only for direct members of the group' do + expect(UserProjectAccessChangedService).to receive(:new).with(contain_exactly(group_user.id)).and_call_original + + group.refresh_members_authorized_projects(direct_members_only: true) + end + end + end + end + + describe '#users_ids_of_direct_members' do + let_it_be(:group) { create(:group, :nested) } + let_it_be(:parent_group_user) { create(:user) } + let_it_be(:group_user) { create(:user) } + + before do + group.parent.add_maintainer(parent_group_user) + group.add_developer(group_user) + end + + it 'does not return user ids of the members of the parent' do + expect(group.users_ids_of_direct_members).not_to include(parent_group_user.id) + end + + it 'returns the user ids of the direct member of the group' do + expect(group.users_ids_of_direct_members).to include(group_user.id) + end + + context 'group sharing' do + let!(:shared_group) { create(:group) } + + before do + create(:group_group_link, shared_group: shared_group, shared_with_group: group) + end + + it 'does not return the user ids of members of the shared_with group' do + expect(shared_group.users_ids_of_direct_members).not_to( + include(group_user.id)) + end + end + end + describe '#user_ids_for_project_authorizations' do it 'returns the user IDs for which to refresh authorizations' do maintainer = create(:user) @@ -959,6 +1105,29 @@ RSpec.describe Group do include(group_user.id)) end end + + context 'distinct user ids' do + let_it_be(:subgroup) { create(:group, :nested) } + let_it_be(:user) { create(:user) } + let_it_be(:shared_with_group) { create(:group) } + let_it_be(:other_subgroup_user) { create(:user) } + + before do + create(:group_group_link, shared_group: subgroup, shared_with_group: shared_with_group) + subgroup.add_maintainer(other_subgroup_user) + + # `user` is added as a direct member of the parent group, the subgroup + # and another group shared with the subgroup. + subgroup.parent.add_maintainer(user) + subgroup.add_developer(user) + shared_with_group.add_guest(user) + end + + it 'returns only distinct user ids of users for which to refresh authorizations' do + expect(subgroup.user_ids_for_project_authorizations).to( + contain_exactly(user.id, other_subgroup_user.id)) + end + end end describe '#update_two_factor_requirement' do @@ -1149,9 +1318,10 @@ RSpec.describe Group do describe '#ci_variables_for' do let(:project) { create(:project, group: group) } + let(:environment_scope) { '*' } let!(:ci_variable) do - create(:ci_group_variable, value: 'secret', group: group) + create(:ci_group_variable, value: 'secret', group: group, environment_scope: environment_scope) end let!(:protected_variable) do @@ -1160,13 +1330,16 @@ RSpec.describe Group do subject { group.ci_variables_for('ref', project) } - it 'memoizes the result by ref', :request_store do + it 'memoizes the result by ref and environment', :request_store do + scoped_variable = create(:ci_group_variable, value: 'secret', group: group, environment_scope: 'scoped') + expect(project).to receive(:protected_for?).with('ref').once.and_return(true) - expect(project).to receive(:protected_for?).with('other').once.and_return(false) + expect(project).to receive(:protected_for?).with('other').twice.and_return(false) 2.times do - expect(group.ci_variables_for('ref', project)).to contain_exactly(ci_variable, protected_variable) + expect(group.ci_variables_for('ref', project, environment: 'production')).to contain_exactly(ci_variable, protected_variable) expect(group.ci_variables_for('other', project)).to contain_exactly(ci_variable) + expect(group.ci_variables_for('other', project, environment: 'scoped')).to contain_exactly(ci_variable, scoped_variable) end end @@ -1203,6 +1376,120 @@ RSpec.describe Group do it_behaves_like 'ref is protected' end + context 'when environment name is specified' do + let(:environment) { 'review/name' } + + subject do + group.ci_variables_for('ref', project, environment: environment) + end + + context 'when environment scope is exactly matched' do + let(:environment_scope) { 'review/name' } + + it { is_expected.to contain_exactly(ci_variable) } + end + + context 'when environment scope is matched by wildcard' do + let(:environment_scope) { 'review/*' } + + it { is_expected.to contain_exactly(ci_variable) } + end + + context 'when environment scope does not match' do + let(:environment_scope) { 'review/*/special' } + + it { is_expected.not_to contain_exactly(ci_variable) } + end + + context 'when environment scope has _' do + let(:environment_scope) { '*_*' } + + it 'does not treat it as wildcard' do + is_expected.not_to contain_exactly(ci_variable) + end + + context 'when environment name contains underscore' do + let(:environment) { 'foo_bar/test' } + let(:environment_scope) { 'foo_bar/*' } + + it 'matches literally for _' do + is_expected.to contain_exactly(ci_variable) + end + end + end + + # The environment name and scope cannot have % at the moment, + # but we're considering relaxing it and we should also make sure + # it doesn't break in case some data sneaked in somehow as we're + # not checking this integrity in database level. + context 'when environment scope has %' do + it 'does not treat it as wildcard' do + ci_variable.update_attribute(:environment_scope, '*%*') + + is_expected.not_to contain_exactly(ci_variable) + end + + context 'when environment name contains a percent' do + let(:environment) { 'foo%bar/test' } + + it 'matches literally for %' do + ci_variable.update(environment_scope: 'foo%bar/*') + + is_expected.to contain_exactly(ci_variable) + end + end + end + + context 'when variables with the same name have different environment scopes' do + let!(:partially_matched_variable) do + create(:ci_group_variable, + key: ci_variable.key, + value: 'partial', + environment_scope: 'review/*', + group: group) + end + + let!(:perfectly_matched_variable) do + create(:ci_group_variable, + key: ci_variable.key, + value: 'prefect', + environment_scope: 'review/name', + group: group) + end + + it 'puts variables matching environment scope more in the end' do + is_expected.to eq( + [ci_variable, + partially_matched_variable, + perfectly_matched_variable]) + end + end + + context 'when :scoped_group_variables feature flag is disabled' do + before do + stub_feature_flags(scoped_group_variables: false) + end + + context 'when environment scope is exactly matched' do + let(:environment_scope) { 'review/name' } + + it { is_expected.to contain_exactly(ci_variable) } + end + + context 'when environment scope is partially matched' do + let(:environment_scope) { 'review/*' } + + it { is_expected.to contain_exactly(ci_variable) } + end + + context 'when environment scope does not match' do + let(:environment_scope) { 'review/*/special' } + + it { is_expected.to contain_exactly(ci_variable) } + end + end + end + context 'when group has children' do let(:group_child) { create(:group, parent: group) } let(:group_child_2) { create(:group, parent: group_child) } diff --git a/spec/models/issue_email_participant_spec.rb b/spec/models/issue_email_participant_spec.rb index f19e65e31f3..09c231bbfda 100644 --- a/spec/models/issue_email_participant_spec.rb +++ b/spec/models/issue_email_participant_spec.rb @@ -11,9 +11,14 @@ RSpec.describe IssueEmailParticipant do subject { build(:issue_email_participant) } it { is_expected.to validate_presence_of(:issue) } - it { is_expected.to validate_presence_of(:email) } - it { is_expected.to validate_uniqueness_of(:email).scoped_to([:issue_id]) } + it { is_expected.to validate_uniqueness_of(:email).scoped_to([:issue_id]).ignoring_case_sensitivity } - it_behaves_like 'an object with RFC3696 compliant email-formated attributes', :email + it_behaves_like 'an object with RFC3696 compliant email-formatted attributes', :email + + it 'is invalid if the email is nil' do + subject.email = nil + + expect(subject).to be_invalid + end end end diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb index 969d897e551..965da59a0ed 100644 --- a/spec/models/issue_spec.rb +++ b/spec/models/issue_spec.rb @@ -1258,4 +1258,12 @@ RSpec.describe Issue do expect { issue.issue_type_supports?(:unkown_feature) }.to raise_error(ArgumentError) end end + + describe '#email_participants_downcase' do + it 'returns a list of emails with all uppercase letters replaced with their lowercase counterparts' do + participant = create(:issue_email_participant, email: 'SomEoNe@ExamPLe.com') + + expect(participant.issue.email_participants_downcase).to match([participant.email.downcase]) + end + end end diff --git a/spec/models/iteration_spec.rb b/spec/models/iteration_spec.rb deleted file mode 100644 index e7ec5de0ef1..00000000000 --- a/spec/models/iteration_spec.rb +++ /dev/null @@ -1,335 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Iteration do - let_it_be(:project) { create(:project) } - let_it_be(:group) { create(:group) } - - describe "#iid" do - it "is properly scoped on project and group" do - iteration1 = create(:iteration, :skip_project_validation, project: project) - iteration2 = create(:iteration, :skip_project_validation, project: project) - iteration3 = create(:iteration, group: group) - iteration4 = create(:iteration, group: group) - iteration5 = create(:iteration, :skip_project_validation, project: project) - - want = { - iteration1: 1, - iteration2: 2, - iteration3: 1, - iteration4: 2, - iteration5: 3 - } - got = { - iteration1: iteration1.iid, - iteration2: iteration2.iid, - iteration3: iteration3.iid, - iteration4: iteration4.iid, - iteration5: iteration5.iid - } - expect(got).to eq(want) - end - end - - describe '.filter_by_state' do - let_it_be(:closed_iteration) { create(:iteration, :closed, :skip_future_date_validation, group: group, start_date: 8.days.ago, due_date: 2.days.ago) } - let_it_be(:started_iteration) { create(:iteration, :started, :skip_future_date_validation, group: group, start_date: 1.day.ago, due_date: 6.days.from_now) } - let_it_be(:upcoming_iteration) { create(:iteration, :upcoming, group: group, start_date: 1.week.from_now, due_date: 2.weeks.from_now) } - - shared_examples_for 'filter_by_state' do - it 'filters by the given state' do - expect(described_class.filter_by_state(Iteration.all, state)).to match(expected_iterations) - end - end - - context 'filtering by closed iterations' do - it_behaves_like 'filter_by_state' do - let(:state) { 'closed' } - let(:expected_iterations) { [closed_iteration] } - end - end - - context 'filtering by started iterations' do - it_behaves_like 'filter_by_state' do - let(:state) { 'started' } - let(:expected_iterations) { [started_iteration] } - end - end - - context 'filtering by opened iterations' do - it_behaves_like 'filter_by_state' do - let(:state) { 'opened' } - let(:expected_iterations) { [started_iteration, upcoming_iteration] } - end - end - - context 'filtering by upcoming iterations' do - it_behaves_like 'filter_by_state' do - let(:state) { 'upcoming' } - let(:expected_iterations) { [upcoming_iteration] } - end - end - - context 'filtering by "all"' do - it_behaves_like 'filter_by_state' do - let(:state) { 'all' } - let(:expected_iterations) { [closed_iteration, started_iteration, upcoming_iteration] } - end - end - - context 'filtering by nonexistent filter' do - it 'raises ArgumentError' do - expect { described_class.filter_by_state(Iteration.none, 'unknown') }.to raise_error(ArgumentError, 'Unknown state filter: unknown') - end - end - end - - context 'Validations' do - subject { build(:iteration, group: group, start_date: start_date, due_date: due_date) } - - describe '#not_belonging_to_project' do - subject { build(:iteration, project: project, start_date: Time.current, due_date: 1.day.from_now) } - - it 'is invalid' do - expect(subject).not_to be_valid - expect(subject.errors[:project_id]).to include('is not allowed. We do not currently support project-level iterations') - end - end - - describe '#dates_do_not_overlap' do - let_it_be(:existing_iteration) { create(:iteration, group: group, start_date: 4.days.from_now, due_date: 1.week.from_now) } - - context 'when no Iteration dates overlap' do - let(:start_date) { 2.weeks.from_now } - let(:due_date) { 3.weeks.from_now } - - it { is_expected.to be_valid } - end - - context 'when updated iteration dates overlap with its own dates' do - it 'is valid' do - existing_iteration.start_date = 5.days.from_now - - expect(existing_iteration).to be_valid - end - end - - context 'when dates overlap' do - let(:start_date) { 5.days.from_now } - let(:due_date) { 6.days.from_now } - - shared_examples_for 'overlapping dates' do |skip_constraint_test: false| - context 'when start_date is in range' do - let(:start_date) { 5.days.from_now } - let(:due_date) { 3.weeks.from_now } - - it 'is not valid' do - expect(subject).not_to be_valid - expect(subject.errors[:base]).to include('Dates cannot overlap with other existing Iterations') - end - - unless skip_constraint_test - it 'is not valid even if forced' do - subject.validate # to generate iid/etc - expect { subject.save!(validate: false) }.to raise_exception(ActiveRecord::StatementInvalid, /#{constraint_name}/) - end - end - end - - context 'when end_date is in range' do - let(:start_date) { Time.current } - let(:due_date) { 6.days.from_now } - - it 'is not valid' do - expect(subject).not_to be_valid - expect(subject.errors[:base]).to include('Dates cannot overlap with other existing Iterations') - end - - unless skip_constraint_test - it 'is not valid even if forced' do - subject.validate # to generate iid/etc - expect { subject.save!(validate: false) }.to raise_exception(ActiveRecord::StatementInvalid, /#{constraint_name}/) - end - end - end - - context 'when both overlap' do - it 'is not valid' do - expect(subject).not_to be_valid - expect(subject.errors[:base]).to include('Dates cannot overlap with other existing Iterations') - end - - unless skip_constraint_test - it 'is not valid even if forced' do - subject.validate # to generate iid/etc - expect { subject.save!(validate: false) }.to raise_exception(ActiveRecord::StatementInvalid, /#{constraint_name}/) - end - end - end - end - - context 'group' do - it_behaves_like 'overlapping dates' do - let(:constraint_name) { 'iteration_start_and_due_daterange_group_id_constraint' } - end - - context 'different group' do - let(:group) { create(:group) } - - it { is_expected.to be_valid } - - it 'does not trigger exclusion constraints' do - expect { subject.save! }.not_to raise_exception - end - end - - context 'sub-group' do - let(:subgroup) { create(:group, parent: group) } - - subject { build(:iteration, group: subgroup, start_date: start_date, due_date: due_date) } - - it_behaves_like 'overlapping dates', skip_constraint_test: true - end - end - - context 'project' do - let_it_be(:existing_iteration) { create(:iteration, :skip_project_validation, project: project, start_date: 4.days.from_now, due_date: 1.week.from_now) } - - subject { build(:iteration, :skip_project_validation, project: project, start_date: start_date, due_date: due_date) } - - it_behaves_like 'overlapping dates' do - let(:constraint_name) { 'iteration_start_and_due_daterange_project_id_constraint' } - end - - context 'different project' do - let(:project) { create(:project) } - - it { is_expected.to be_valid } - - it 'does not trigger exclusion constraints' do - expect { subject.save! }.not_to raise_exception - end - end - - context 'in a group' do - let(:group) { create(:group) } - - subject { build(:iteration, group: group, start_date: start_date, due_date: due_date) } - - it { is_expected.to be_valid } - - it 'does not trigger exclusion constraints' do - expect { subject.save! }.not_to raise_exception - end - end - end - - context 'project in a group' do - let_it_be(:project) { create(:project, group: create(:group)) } - let_it_be(:existing_iteration) { create(:iteration, :skip_project_validation, project: project, start_date: 4.days.from_now, due_date: 1.week.from_now) } - - subject { build(:iteration, :skip_project_validation, project: project, start_date: start_date, due_date: due_date) } - - it_behaves_like 'overlapping dates' do - let(:constraint_name) { 'iteration_start_and_due_daterange_project_id_constraint' } - end - end - end - end - - describe '#future_date' do - context 'when dates are in the future' do - let(:start_date) { Time.current } - let(:due_date) { 1.week.from_now } - - it { is_expected.to be_valid } - end - - context 'when start_date is in the past' do - let(:start_date) { 1.week.ago } - let(:due_date) { 1.week.from_now } - - it 'is not valid' do - expect(subject).not_to be_valid - expect(subject.errors[:start_date]).to include('cannot be in the past') - end - end - - context 'when due_date is in the past' do - let(:start_date) { Time.current } - let(:due_date) { 1.week.ago } - - it 'is not valid' do - expect(subject).not_to be_valid - expect(subject.errors[:due_date]).to include('cannot be in the past') - end - end - - context 'when start_date is over 500 years in the future' do - let(:start_date) { 501.years.from_now } - let(:due_date) { Time.current } - - it 'is not valid' do - expect(subject).not_to be_valid - expect(subject.errors[:start_date]).to include('cannot be more than 500 years in the future') - end - end - - context 'when due_date is over 500 years in the future' do - let(:start_date) { Time.current } - let(:due_date) { 501.years.from_now } - - it 'is not valid' do - expect(subject).not_to be_valid - expect(subject.errors[:due_date]).to include('cannot be more than 500 years in the future') - end - end - end - end - - context 'time scopes' do - let_it_be(:project) { create(:project, :empty_repo) } - let_it_be(:iteration_1) { create(:iteration, :skip_future_date_validation, :skip_project_validation, project: project, start_date: 3.days.ago, due_date: 1.day.from_now) } - let_it_be(:iteration_2) { create(:iteration, :skip_future_date_validation, :skip_project_validation, project: project, start_date: 10.days.ago, due_date: 4.days.ago) } - let_it_be(:iteration_3) { create(:iteration, :skip_project_validation, project: project, start_date: 4.days.from_now, due_date: 1.week.from_now) } - - describe 'start_date_passed' do - it 'returns iterations where start_date is in the past but due_date is in the future' do - expect(described_class.start_date_passed).to contain_exactly(iteration_1) - end - end - - describe 'due_date_passed' do - it 'returns iterations where due date is in the past' do - expect(described_class.due_date_passed).to contain_exactly(iteration_2) - end - end - end - - describe '.within_timeframe' do - let_it_be(:now) { Time.current } - let_it_be(:project) { create(:project, :empty_repo) } - let_it_be(:iteration_1) { create(:iteration, :skip_project_validation, project: project, start_date: now, due_date: 1.day.from_now) } - let_it_be(:iteration_2) { create(:iteration, :skip_project_validation, project: project, start_date: 2.days.from_now, due_date: 3.days.from_now) } - let_it_be(:iteration_3) { create(:iteration, :skip_project_validation, project: project, start_date: 4.days.from_now, due_date: 1.week.from_now) } - - it 'returns iterations with start_date and/or end_date between timeframe' do - iterations = described_class.within_timeframe(2.days.from_now, 3.days.from_now) - - expect(iterations).to match_array([iteration_2]) - end - - it 'returns iterations which starts before the timeframe' do - iterations = described_class.within_timeframe(1.day.from_now, 3.days.from_now) - - expect(iterations).to match_array([iteration_1, iteration_2]) - end - - it 'returns iterations which ends after the timeframe' do - iterations = described_class.within_timeframe(3.days.from_now, 5.days.from_now) - - expect(iterations).to match_array([iteration_2, iteration_3]) - end - end -end diff --git a/spec/models/list_spec.rb b/spec/models/list_spec.rb index f0b1bc33e84..ad07ee1115b 100644 --- a/spec/models/list_spec.rb +++ b/spec/models/list_spec.rb @@ -5,6 +5,7 @@ require 'spec_helper' RSpec.describe List do it_behaves_like 'having unique enum values' it_behaves_like 'boards listable model', :list + it_behaves_like 'list_preferences_for user', :list, :list_id describe 'relationships' do it { is_expected.to belong_to(:board) } @@ -17,70 +18,16 @@ RSpec.describe List do it { is_expected.to validate_presence_of(:list_type) } end - describe '#update_preferences_for' do - let(:user) { create(:user) } - let(:list) { create(:list) } + describe '.without_types' do + it 'exclude lists of given types' do + board = create(:list, list_type: :label).board + # closed list is created by default + backlog_list = create(:list, list_type: :backlog, board: board) - context 'when user is present' do - context 'when there are no preferences for user' do - it 'creates new user preferences' do - expect { list.update_preferences_for(user, collapsed: true) }.to change { ListUserPreference.count }.by(1) - expect(list.preferences_for(user).collapsed).to eq(true) - end - end + exclude_type = [described_class.list_types[:label], described_class.list_types[:closed]] - context 'when there are preferences for user' do - it 'updates user preferences' do - list.update_preferences_for(user, collapsed: false) - - expect { list.update_preferences_for(user, collapsed: true) }.not_to change { ListUserPreference.count } - expect(list.preferences_for(user).collapsed).to eq(true) - end - end - - context 'when user is nil' do - it 'does not create user preferences' do - expect { list.update_preferences_for(nil, collapsed: true) }.not_to change { ListUserPreference.count } - end - end - end - end - - describe '#preferences_for' do - let(:user) { create(:user) } - let(:list) { create(:list) } - - context 'when user is nil' do - it 'returns not persisted preferences' do - preferences = list.preferences_for(nil) - - expect(preferences.persisted?).to eq(false) - expect(preferences.list_id).to eq(list.id) - expect(preferences.user_id).to be_nil - end - end - - context 'when a user preference already exists' do - before do - list.update_preferences_for(user, collapsed: true) - end - - it 'loads preference for user' do - preferences = list.preferences_for(user) - - expect(preferences).to be_persisted - expect(preferences.collapsed).to eq(true) - end - end - - context 'when preferences for user does not exist' do - it 'returns not persisted preferences' do - preferences = list.preferences_for(user) - - expect(preferences.persisted?).to eq(false) - expect(preferences.user_id).to eq(user.id) - expect(preferences.list_id).to eq(list.id) - end + lists = described_class.without_types(exclude_type) + expect(lists.where(board: board)).to match_array([backlog_list]) end end end diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb index b60af7abade..e918a573e09 100644 --- a/spec/models/member_spec.rb +++ b/spec/models/member_spec.rb @@ -24,7 +24,7 @@ RSpec.describe Member do it { is_expected.to allow_value(nil).for(:expires_at) } end - it_behaves_like 'an object with email-formated attributes', :invite_email do + it_behaves_like 'an object with email-formatted attributes', :invite_email do subject { build(:project_member) } end @@ -425,12 +425,10 @@ RSpec.describe Member do end context 'when admin mode is disabled' do - # Skipped because `Group#max_member_access_for_user` needs to be migrated to use admin mode - # https://gitlab.com/gitlab-org/gitlab/-/issues/207950 - xit 'rejects setting members.created_by to the given admin current_user' do + it 'rejects setting members.created_by to the given admin current_user' do member = described_class.add_user(source, user, :maintainer, current_user: admin) - expect(member.created_by).not_to be_persisted + expect(member.created_by).to be_nil end end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index ebe2cd2ac03..8c7289adbcc 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -9,8 +9,8 @@ RSpec.describe MergeRequest, factory_default: :keep do using RSpec::Parameterized::TableSyntax - let_it_be(:namespace) { create_default(:namespace) } - let_it_be(:project, refind: true) { create_default(:project, :repository) } + let_it_be(:namespace) { create_default(:namespace).freeze } + let_it_be(:project, refind: true) { create_default(:project, :repository).freeze } subject { create(:merge_request) } @@ -1366,6 +1366,10 @@ RSpec.describe MergeRequest, factory_default: :keep do it "doesn't detect WIP by default" do expect(subject.work_in_progress?).to eq false end + + it "is aliased to #draft?" do + expect(subject.method(:work_in_progress?)).to eq(subject.method(:draft?)) + end end describe "#wipless_title" do @@ -2895,6 +2899,14 @@ RSpec.describe MergeRequest, factory_default: :keep do expect(subject.mergeable?).to be_truthy end + it 'return true if #mergeable_state? is true and the MR #can_be_merged? is false' do + allow(subject).to receive(:mergeable_state?) { true } + expect(subject).to receive(:check_mergeability) + expect(subject).to receive(:can_be_merged?) { false } + + expect(subject.mergeable?).to be_falsey + end + context 'with skip_ci_check option' do before do allow(subject).to receive_messages(check_mergeability: nil, @@ -3072,6 +3084,7 @@ RSpec.describe MergeRequest, factory_default: :keep do where(:status, :public_status) do 'cannot_be_merged_rechecking' | 'checking' + 'preparing' | 'checking' 'checking' | 'checking' 'cannot_be_merged' | 'cannot_be_merged' end @@ -3082,32 +3095,83 @@ RSpec.describe MergeRequest, factory_default: :keep do end describe "#head_pipeline_active? " do - it do - is_expected - .to delegate_method(:active?) - .to(:head_pipeline) - .with_prefix - .with_arguments(allow_nil: true) + context 'when project lacks a head_pipeline relation' do + before do + subject.head_pipeline = nil + end + + it 'returns false' do + expect(subject.head_pipeline_active?).to be false + end + end + + context 'when project has a head_pipeline relation' do + let(:pipeline) { create(:ci_empty_pipeline) } + + before do + allow(subject).to receive(:head_pipeline) { pipeline } + end + + it 'accesses the value from the head_pipeline' do + expect(subject.head_pipeline) + .to receive(:active?) + + subject.head_pipeline_active? + end end end describe "#actual_head_pipeline_success? " do - it do - is_expected - .to delegate_method(:success?) - .to(:actual_head_pipeline) - .with_prefix - .with_arguments(allow_nil: true) + context 'when project lacks an actual_head_pipeline relation' do + before do + allow(subject).to receive(:actual_head_pipeline) { nil } + end + + it 'returns false' do + expect(subject.actual_head_pipeline_success?).to be false + end + end + + context 'when project has a actual_head_pipeline relation' do + let(:pipeline) { create(:ci_empty_pipeline) } + + before do + allow(subject).to receive(:actual_head_pipeline) { pipeline } + end + + it 'accesses the value from the actual_head_pipeline' do + expect(subject.actual_head_pipeline) + .to receive(:success?) + + subject.actual_head_pipeline_success? + end end end describe "#actual_head_pipeline_active? " do - it do - is_expected - .to delegate_method(:active?) - .to(:actual_head_pipeline) - .with_prefix - .with_arguments(allow_nil: true) + context 'when project lacks an actual_head_pipeline relation' do + before do + allow(subject).to receive(:actual_head_pipeline) { nil } + end + + it 'returns false' do + expect(subject.actual_head_pipeline_active?).to be false + end + end + + context 'when project has a actual_head_pipeline relation' do + let(:pipeline) { create(:ci_empty_pipeline) } + + before do + allow(subject).to receive(:actual_head_pipeline) { pipeline } + end + + it 'accesses the value from the actual_head_pipeline' do + expect(subject.actual_head_pipeline) + .to receive(:active?) + + subject.actual_head_pipeline_active? + end end end @@ -3784,6 +3848,87 @@ RSpec.describe MergeRequest, factory_default: :keep do end end + describe '#use_merge_base_pipeline_for_comparison?' do + let(:project) { create(:project, :public, :repository) } + let(:merge_request) { create(:merge_request, :with_codequality_reports, source_project: project) } + + subject { merge_request.use_merge_base_pipeline_for_comparison?(service_class) } + + context 'when service class is Ci::CompareCodequalityReportsService' do + let(:service_class) { 'Ci::CompareCodequalityReportsService' } + + context 'when feature flag is enabled' do + it { is_expected.to be_truthy } + end + + context 'when feature flag is disabled' do + before do + stub_feature_flags(codequality_backend_comparison: false) + end + + it { is_expected.to be_falsey } + end + end + + context 'when service class is different' do + let(:service_class) { 'Ci::GenerateCoverageReportsService' } + + it { is_expected.to be_falsey } + end + end + + describe '#comparison_base_pipeline' do + subject(:pipeline) { merge_request.comparison_base_pipeline(service_class) } + + let(:project) { create(:project, :public, :repository) } + let(:merge_request) { create(:merge_request, :with_codequality_reports, source_project: project) } + let!(:base_pipeline) do + create(:ci_pipeline, + :with_test_reports, + project: project, + ref: merge_request.target_branch, + sha: merge_request.diff_base_sha + ) + end + + context 'when service class is Ci::CompareCodequalityReportsService' do + let(:service_class) { 'Ci::CompareCodequalityReportsService' } + + context 'when merge request has a merge request pipeline' do + let(:merge_request) do + create(:merge_request, :with_merge_request_pipeline) + end + + let(:merge_base_pipeline) do + create(:ci_pipeline, ref: merge_request.target_branch, sha: merge_request.target_branch_sha) + end + + before do + merge_base_pipeline + merge_request.update_head_pipeline + end + + it 'returns the merge_base_pipeline' do + expect(pipeline).to eq(merge_base_pipeline) + end + end + + context 'when merge does not have a merge request pipeline' do + it 'returns the base_pipeline' do + expect(pipeline).to eq(base_pipeline) + end + end + end + + context 'when service_class is different' do + let(:service_class) { 'Ci::GenerateCoverageReportsService' } + + it 'returns the base_pipeline' do + expect(pipeline).to eq(base_pipeline) + end + end + end + describe '#base_pipeline' do let(:pipeline_arguments) do { @@ -3963,6 +4108,65 @@ RSpec.describe MergeRequest, factory_default: :keep do end end + describe '#mark_as_unchecked' do + subject { create(:merge_request, source_project: project, merge_status: merge_status) } + + shared_examples 'for an invalid state transition' do + it 'is not a valid state transition' do + expect { subject.mark_as_unchecked! }.to raise_error(StateMachines::InvalidTransition) + end + end + + shared_examples 'for an valid state transition' do + it 'is a valid state transition' do + expect { subject.mark_as_unchecked! } + .to change { subject.merge_status } + .from(merge_status.to_s) + .to(expected_merge_status) + end + end + + context 'when the status is unchecked' do + let(:merge_status) { :unchecked } + + include_examples 'for an invalid state transition' + end + + context 'when the status is checking' do + let(:merge_status) { :checking } + let(:expected_merge_status) { 'unchecked' } + + include_examples 'for an valid state transition' + end + + context 'when the status is can_be_merged' do + let(:merge_status) { :can_be_merged } + let(:expected_merge_status) { 'unchecked' } + + include_examples 'for an valid state transition' + end + + context 'when the status is cannot_be_merged_recheck' do + let(:merge_status) { :cannot_be_merged_recheck } + + include_examples 'for an invalid state transition' + end + + context 'when the status is cannot_be_merged' do + let(:merge_status) { :cannot_be_merged } + let(:expected_merge_status) { 'cannot_be_merged_recheck' } + + include_examples 'for an valid state transition' + end + + context 'when the status is cannot_be_merged' do + let(:merge_status) { :cannot_be_merged } + let(:expected_merge_status) { 'cannot_be_merged_recheck' } + + include_examples 'for an valid state transition' + end + end + describe 'transition to cannot_be_merged' do let(:notification_service) { double(:notification_service) } let(:todo_service) { double(:todo_service) } @@ -4661,4 +4865,33 @@ RSpec.describe MergeRequest, factory_default: :keep do end end end + + describe '#includes_ci_config?' do + let(:merge_request) { build(:merge_request) } + let(:project) { merge_request.project } + + subject(:result) { merge_request.includes_ci_config? } + + before do + allow(merge_request).to receive(:diff_stats).and_return(diff_stats) + end + + context 'when diff_stats is nil' do + let(:diff_stats) {} + + it { is_expected.to eq(false) } + end + + context 'when diff_stats does not include the ci config path of the project' do + let(:diff_stats) { [double(path: 'abc.txt')] } + + it { is_expected.to eq(false) } + end + + context 'when diff_stats includes the ci config path of the project' do + let(:diff_stats) { [double(path: '.gitlab-ci.yml')] } + + it { is_expected.to eq(true) } + end + end end diff --git a/spec/models/namespace/traversal_hierarchy_spec.rb b/spec/models/namespace/traversal_hierarchy_spec.rb index 71b0e974106..83e6d704640 100644 --- a/spec/models/namespace/traversal_hierarchy_spec.rb +++ b/spec/models/namespace/traversal_hierarchy_spec.rb @@ -3,41 +3,41 @@ require 'spec_helper' RSpec.describe Namespace::TraversalHierarchy, type: :model do - let_it_be(:root, reload: true) { create(:namespace, :with_hierarchy) } + let_it_be(:root, reload: true) { create(:group, :with_hierarchy) } describe '.for_namespace' do - let(:hierarchy) { described_class.for_namespace(namespace) } + let(:hierarchy) { described_class.for_namespace(group) } context 'with root group' do - let(:namespace) { root } + let(:group) { root } it { expect(hierarchy.root).to eq root } end context 'with child group' do - let(:namespace) { root.children.first.children.first } + let(:group) { root.children.first.children.first } it { expect(hierarchy.root).to eq root } end context 'with group outside of hierarchy' do - let(:namespace) { create(:namespace) } + let(:group) { create(:namespace) } it { expect(hierarchy.root).not_to eq root } end end describe '.new' do - let(:hierarchy) { described_class.new(namespace) } + let(:hierarchy) { described_class.new(group) } context 'with root group' do - let(:namespace) { root } + let(:group) { root } it { expect(hierarchy.root).to eq root } end context 'with child group' do - let(:namespace) { root.children.first } + let(:group) { root.children.first } it { expect { hierarchy }.to raise_error(StandardError, 'Must specify a root node') } end diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index 647e279bf83..65d787d334b 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -6,7 +6,7 @@ RSpec.describe Namespace do include ProjectForksHelper include GitHelpers - let!(:namespace) { create(:namespace) } + let!(:namespace) { create(:namespace, :with_namespace_settings) } let(:gitlab_shell) { Gitlab::Shell.new } let(:repository_storage) { 'default' } @@ -32,14 +32,68 @@ RSpec.describe Namespace do it { is_expected.to validate_presence_of(:owner) } it { is_expected.to validate_numericality_of(:max_artifacts_size).only_integer.is_greater_than(0) } + context 'validating the parent of a namespace' do + context 'when the namespace has no parent' do + it 'allows a namespace to have no parent associated with it' do + namespace = build(:namespace) + + expect(namespace).to be_valid + end + end + + context 'when the namespace has a parent' do + it 'does not allow a namespace to have a group as its parent' do + namespace = build(:namespace, parent: build(:group)) + + expect(namespace).not_to be_valid + expect(namespace.errors[:parent_id].first).to eq('a user namespace cannot have a parent') + end + + it 'does not allow a namespace to have another namespace as its parent' do + namespace = build(:namespace, parent: build(:namespace)) + + expect(namespace).not_to be_valid + expect(namespace.errors[:parent_id].first).to eq('a user namespace cannot have a parent') + end + end + + context 'when the feature flag `validate_namespace_parent_type` is disabled' do + before do + stub_feature_flags(validate_namespace_parent_type: false) + end + + context 'when the namespace has no parent' do + it 'allows a namespace to have no parent associated with it' do + namespace = build(:namespace) + + expect(namespace).to be_valid + end + end + + context 'when the namespace has a parent' do + it 'allows a namespace to have a group as its parent' do + namespace = build(:namespace, parent: build(:group)) + + expect(namespace).to be_valid + end + + it 'allows a namespace to have another namespace as its parent' do + namespace = build(:namespace, parent: build(:namespace)) + + expect(namespace).to be_valid + end + end + end + end + it 'does not allow too deep nesting' do ancestors = (1..21).to_a - nested = build(:namespace, parent: namespace) + group = build(:group) - allow(nested).to receive(:ancestors).and_return(ancestors) + allow(group).to receive(:ancestors).and_return(ancestors) - expect(nested).not_to be_valid - expect(nested.errors[:parent_id].first).to eq('has too deep level of nesting') + expect(group).not_to be_valid + expect(group.errors[:parent_id].first).to eq('has too deep level of nesting') end describe 'reserved path validation' do @@ -116,6 +170,28 @@ RSpec.describe Namespace do it { is_expected.to include_module(Namespaces::Traversal::Recursive) } end + describe 'callbacks' do + describe 'before_save :ensure_delayed_project_removal_assigned_to_namespace_settings' do + it 'sets the matching value in namespace_settings' do + expect { namespace.update!(delayed_project_removal: true) }.to change { + namespace.namespace_settings.delayed_project_removal + }.from(false).to(true) + end + + context 'when the feature flag is disabled' do + before do + stub_feature_flags(migrate_delayed_project_removal: false) + end + + it 'does not set the matching value in namespace_settings' do + expect { namespace.update!(delayed_project_removal: true) }.not_to change { + namespace.namespace_settings.delayed_project_removal + } + end + end + end + end + describe '#visibility_level_field' do it { expect(namespace.visibility_level_field).to eq(:visibility_level) } end @@ -150,45 +226,45 @@ RSpec.describe Namespace do end describe '.search' do - let_it_be(:first_namespace) { build(:namespace, name: 'my first namespace', path: 'old-path').tap(&:save!) } - let_it_be(:parent_namespace) { build(:namespace, name: 'my parent namespace', path: 'parent-path').tap(&:save!) } - let_it_be(:second_namespace) { build(:namespace, name: 'my second namespace', path: 'new-path', parent: parent_namespace).tap(&:save!) } - let_it_be(:project_with_same_path) { create(:project, id: second_namespace.id, path: first_namespace.path) } + let_it_be(:first_group) { build(:group, name: 'my first namespace', path: 'old-path').tap(&:save!) } + let_it_be(:parent_group) { build(:group, name: 'my parent namespace', path: 'parent-path').tap(&:save!) } + let_it_be(:second_group) { build(:group, name: 'my second namespace', path: 'new-path', parent: parent_group).tap(&:save!) } + let_it_be(:project_with_same_path) { create(:project, id: second_group.id, path: first_group.path) } it 'returns namespaces with a matching name' do - expect(described_class.search('my first namespace')).to eq([first_namespace]) + expect(described_class.search('my first namespace')).to eq([first_group]) end it 'returns namespaces with a partially matching name' do - expect(described_class.search('first')).to eq([first_namespace]) + expect(described_class.search('first')).to eq([first_group]) end it 'returns namespaces with a matching name regardless of the casing' do - expect(described_class.search('MY FIRST NAMESPACE')).to eq([first_namespace]) + expect(described_class.search('MY FIRST NAMESPACE')).to eq([first_group]) end it 'returns namespaces with a matching path' do - expect(described_class.search('old-path')).to eq([first_namespace]) + expect(described_class.search('old-path')).to eq([first_group]) end it 'returns namespaces with a partially matching path' do - expect(described_class.search('old')).to eq([first_namespace]) + expect(described_class.search('old')).to eq([first_group]) end it 'returns namespaces with a matching path regardless of the casing' do - expect(described_class.search('OLD-PATH')).to eq([first_namespace]) + expect(described_class.search('OLD-PATH')).to eq([first_group]) end it 'returns namespaces with a matching route path' do - expect(described_class.search('parent-path/new-path', include_parents: true)).to eq([second_namespace]) + expect(described_class.search('parent-path/new-path', include_parents: true)).to eq([second_group]) end it 'returns namespaces with a partially matching route path' do - expect(described_class.search('parent-path/new', include_parents: true)).to eq([second_namespace]) + expect(described_class.search('parent-path/new', include_parents: true)).to eq([second_group]) end it 'returns namespaces with a matching route path regardless of the casing' do - expect(described_class.search('PARENT-PATH/NEW-PATH', include_parents: true)).to eq([second_namespace]) + expect(described_class.search('PARENT-PATH/NEW-PATH', include_parents: true)).to eq([second_group]) end end @@ -285,6 +361,18 @@ RSpec.describe Namespace do end end + describe '.top_most' do + let_it_be(:namespace) { create(:namespace) } + let_it_be(:group) { create(:group) } + let_it_be(:subgroup) { create(:group, parent: group) } + + subject { described_class.top_most.ids } + + it 'only contains root namespaces' do + is_expected.to contain_exactly(group.id, namespace.id) + end + end + describe '#ancestors_upto' do let(:parent) { create(:group) } let(:child) { create(:group, parent: parent) } @@ -800,7 +888,7 @@ RSpec.describe Namespace do end describe '#all_projects' do - shared_examples 'all projects for a group' do + context 'when namespace is a group' do let(:namespace) { create(:group) } let(:child) { create(:group, parent: namespace) } let!(:project1) { create(:project_empty_repo, namespace: namespace) } @@ -808,49 +896,39 @@ RSpec.describe Namespace do it { expect(namespace.all_projects.to_a).to match_array([project2, project1]) } it { expect(child.all_projects.to_a).to match_array([project2]) } - end - - shared_examples 'all projects for personal namespace' do - let_it_be(:user) { create(:user) } - let_it_be(:user_namespace) { create(:namespace, owner: user) } - let_it_be(:project) { create(:project, namespace: user_namespace) } - it { expect(user_namespace.all_projects.to_a).to match_array([project]) } - end - - context 'with recursive approach' do - context 'when namespace is a group' do - include_examples 'all projects for a group' + context 'when recursive_namespace_lookup_as_inner_join feature flag is on' do + before do + stub_feature_flags(recursive_namespace_lookup_as_inner_join: true) + end it 'queries for the namespace and its descendants' do - expect(Project).to receive(:where).with(namespace: [namespace, child]) - - namespace.all_projects + expect(namespace.all_projects).to match_array([project1, project2]) end end - context 'when namespace is a user namespace' do - include_examples 'all projects for personal namespace' - - it 'only queries for the namespace itself' do - expect(Project).to receive(:where).with(namespace: user_namespace) + context 'when recursive_namespace_lookup_as_inner_join feature flag is off' do + before do + stub_feature_flags(recursive_namespace_lookup_as_inner_join: false) + end - user_namespace.all_projects + it 'queries for the namespace and its descendants' do + expect(namespace.all_projects).to match_array([project1, project2]) end end end - context 'with route path wildcard approach' do - before do - stub_feature_flags(recursive_approach_for_all_projects: false) - end + context 'when namespace is a user namespace' do + let_it_be(:user) { create(:user) } + let_it_be(:user_namespace) { create(:namespace, owner: user) } + let_it_be(:project) { create(:project, namespace: user_namespace) } - context 'when namespace is a group' do - include_examples 'all projects for a group' - end + it { expect(user_namespace.all_projects.to_a).to match_array([project]) } + + it 'only queries for the namespace itself' do + expect(Project).to receive(:where).with(namespace: user_namespace) - context 'when namespace is a user namespace' do - include_examples 'all projects for personal namespace' + user_namespace.all_projects end end end @@ -1250,14 +1328,14 @@ RSpec.describe Namespace do using RSpec::Parameterized::TableSyntax shared_examples_for 'fetching closest setting' do - let!(:root_namespace) { create(:namespace) } - let!(:namespace) { create(:namespace, parent: root_namespace) } + let!(:parent) { create(:group) } + let!(:group) { create(:group, parent: parent) } - let(:setting) { namespace.closest_setting(setting_name) } + let(:setting) { group.closest_setting(setting_name) } before do - root_namespace.update_attribute(setting_name, root_setting) - namespace.update_attribute(setting_name, child_setting) + parent.update_attribute(setting_name, root_setting) + group.update_attribute(setting_name, child_setting) end it 'returns closest non-nil value' do @@ -1348,30 +1426,30 @@ RSpec.describe Namespace do context 'with a parent' do context 'when parent has shared runners disabled' do - let(:parent) { create(:namespace, :shared_runners_disabled) } - let(:sub_namespace) { build(:namespace, shared_runners_enabled: true, parent_id: parent.id) } + let(:parent) { create(:group, :shared_runners_disabled) } + let(:group) { build(:group, shared_runners_enabled: true, parent_id: parent.id) } it 'is invalid' do - expect(sub_namespace).to be_invalid - expect(sub_namespace.errors[:shared_runners_enabled]).to include('cannot be enabled because parent group has shared Runners disabled') + expect(group).to be_invalid + expect(group.errors[:shared_runners_enabled]).to include('cannot be enabled because parent group has shared Runners disabled') end end context 'when parent has shared runners disabled but allows override' do - let(:parent) { create(:namespace, :shared_runners_disabled, :allow_descendants_override_disabled_shared_runners) } - let(:sub_namespace) { build(:namespace, shared_runners_enabled: true, parent_id: parent.id) } + let(:parent) { create(:group, :shared_runners_disabled, :allow_descendants_override_disabled_shared_runners) } + let(:group) { build(:group, shared_runners_enabled: true, parent_id: parent.id) } it 'is valid' do - expect(sub_namespace).to be_valid + expect(group).to be_valid end end context 'when parent has shared runners enabled' do - let(:parent) { create(:namespace, shared_runners_enabled: true) } - let(:sub_namespace) { build(:namespace, shared_runners_enabled: true, parent_id: parent.id) } + let(:parent) { create(:group, shared_runners_enabled: true) } + let(:group) { build(:group, shared_runners_enabled: true, parent_id: parent.id) } it 'is valid' do - expect(sub_namespace).to be_valid + expect(group).to be_valid end end end @@ -1401,30 +1479,30 @@ RSpec.describe Namespace do context 'with a parent' do context 'when parent does not allow shared runners' do - let(:parent) { create(:namespace, :shared_runners_disabled) } - let(:sub_namespace) { build(:namespace, :shared_runners_disabled, :allow_descendants_override_disabled_shared_runners, parent_id: parent.id) } + let(:parent) { create(:group, :shared_runners_disabled) } + let(:group) { build(:group, :shared_runners_disabled, :allow_descendants_override_disabled_shared_runners, parent_id: parent.id) } it 'is invalid' do - expect(sub_namespace).to be_invalid - expect(sub_namespace.errors[:allow_descendants_override_disabled_shared_runners]).to include('cannot be enabled because parent group does not allow it') + expect(group).to be_invalid + expect(group.errors[:allow_descendants_override_disabled_shared_runners]).to include('cannot be enabled because parent group does not allow it') end end context 'when parent allows shared runners and setting to true' do - let(:parent) { create(:namespace, shared_runners_enabled: true) } - let(:sub_namespace) { build(:namespace, :shared_runners_disabled, :allow_descendants_override_disabled_shared_runners, parent_id: parent.id) } + let(:parent) { create(:group, shared_runners_enabled: true) } + let(:group) { build(:group, :shared_runners_disabled, :allow_descendants_override_disabled_shared_runners, parent_id: parent.id) } it 'is valid' do - expect(sub_namespace).to be_valid + expect(group).to be_valid end end context 'when parent allows shared runners and setting to false' do - let(:parent) { create(:namespace, shared_runners_enabled: true) } - let(:sub_namespace) { build(:namespace, :shared_runners_disabled, allow_descendants_override_disabled_shared_runners: false, parent_id: parent.id) } + let(:parent) { create(:group, shared_runners_enabled: true) } + let(:group) { build(:group, :shared_runners_disabled, allow_descendants_override_disabled_shared_runners: false, parent_id: parent.id) } it 'is valid' do - expect(sub_namespace).to be_valid + expect(group).to be_valid end end end @@ -1449,4 +1527,24 @@ RSpec.describe Namespace do end end end + + describe '#recent?' do + subject { namespace.recent? } + + context 'when created more than 90 days ago' do + before do + namespace.update_attribute(:created_at, 91.days.ago) + end + + it { is_expected.to be(false) } + end + + context 'when created less than 90 days ago' do + before do + namespace.update_attribute(:created_at, 89.days.ago) + end + + it { is_expected.to be(true) } + end + end end diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb index 364b80e8601..590acfc0ac1 100644 --- a/spec/models/note_spec.rb +++ b/spec/models/note_spec.rb @@ -336,6 +336,25 @@ RSpec.describe Note do end end + describe "last_edited_at" do + let(:timestamp) { Time.current } + let(:note) { build(:note, last_edited_at: nil, created_at: timestamp, updated_at: timestamp + 5.hours) } + + context "with last_edited_at" do + it "returns last_edited_at" do + note.last_edited_at = timestamp + + expect(note.last_edited_at).to eq(timestamp) + end + end + + context "without last_edited_at" do + it "returns updated_at" do + expect(note.last_edited_at).to eq(timestamp + 5.hours) + end + end + end + describe "edited?" do let(:note) { build(:note, updated_by_id: nil, created_at: Time.current, updated_at: Time.current + 5.hours) } diff --git a/spec/models/notification_recipient_spec.rb b/spec/models/notification_recipient_spec.rb index 8429f577dc6..4debda0621c 100644 --- a/spec/models/notification_recipient_spec.rb +++ b/spec/models/notification_recipient_spec.rb @@ -337,6 +337,39 @@ RSpec.describe NotificationRecipient do expect(recipient.suitable_notification_level?).to eq true end end + + context 'with merge_when_pipeline_succeeds' do + let(:notification_setting) { user.notification_settings_for(project) } + let(:recipient) do + described_class.new( + user, + :watch, + custom_action: :merge_when_pipeline_succeeds, + target: target, + project: project + ) + end + + context 'custom event enabled' do + before do + notification_setting.update!(merge_when_pipeline_succeeds: true) + end + + it 'returns true' do + expect(recipient.suitable_notification_level?).to eq true + end + end + + context 'custom event disabled' do + before do + notification_setting.update!(merge_when_pipeline_succeeds: false) + end + + it 'returns false' do + expect(recipient.suitable_notification_level?).to eq false + end + end + end end end diff --git a/spec/models/notification_setting_spec.rb b/spec/models/notification_setting_spec.rb index bc50e2af373..4ef5ab7af48 100644 --- a/spec/models/notification_setting_spec.rb +++ b/spec/models/notification_setting_spec.rb @@ -180,7 +180,8 @@ RSpec.describe NotificationSetting do :failed_pipeline, :success_pipeline, :fixed_pipeline, - :moved_project + :moved_project, + :merge_when_pipeline_succeeds ) end diff --git a/spec/models/onboarding_progress_spec.rb b/spec/models/onboarding_progress_spec.rb index 0aa19345a25..779312c9fa0 100644 --- a/spec/models/onboarding_progress_spec.rb +++ b/spec/models/onboarding_progress_spec.rb @@ -106,7 +106,7 @@ RSpec.describe OnboardingProgress do end context 'when not given a root namespace' do - let(:namespace) { create(:namespace, parent: build(:namespace)) } + let(:namespace) { create(:group, parent: build(:group)) } it 'does not add a record for the namespace' do expect { onboard }.not_to change(described_class, :count).from(0) @@ -182,6 +182,30 @@ RSpec.describe OnboardingProgress do end end + describe '.not_completed?' do + subject { described_class.not_completed?(namespace.id, action) } + + context 'when the namespace has not yet been onboarded' do + it { is_expected.to be(false) } + end + + context 'when the namespace has been onboarded but not registered the action yet' do + before do + described_class.onboard(namespace) + end + + it { is_expected.to be(true) } + + context 'when the action has been registered' do + before do + described_class.register(namespace, action) + end + + it { is_expected.to be(false) } + end + end + end + describe '.column_name' do subject { described_class.column_name(action) } diff --git a/spec/models/packages/package_file_spec.rb b/spec/models/packages/package_file_spec.rb index ebb10e991ad..9cf998a0639 100644 --- a/spec/models/packages/package_file_spec.rb +++ b/spec/models/packages/package_file_spec.rb @@ -62,6 +62,21 @@ RSpec.describe Packages::PackageFile, type: :model do end end + describe '.for_rubygem_with_file_name' do + let_it_be(:project) { create(:project) } + let_it_be(:non_ruby_package) { create(:nuget_package, project: project, package_type: :nuget) } + let_it_be(:ruby_package) { create(:rubygems_package, project: project, package_type: :rubygems) } + let_it_be(:file_name) { 'other.gem' } + + let_it_be(:non_ruby_file) { create(:package_file, :nuget, package: non_ruby_package, file_name: file_name) } + let_it_be(:gem_file1) { create(:package_file, :gem, package: ruby_package) } + let_it_be(:gem_file2) { create(:package_file, :gem, package: ruby_package, file_name: file_name) } + + it 'returns the matching gem file only for ruby packages' do + expect(described_class.for_rubygem_with_file_name(project, file_name)).to contain_exactly(gem_file2) + end + end + describe '#update_file_store callback' do let_it_be(:package_file) { build(:package_file, :nuget, size: nil) } diff --git a/spec/models/packages/package_spec.rb b/spec/models/packages/package_spec.rb index 6c55d37b95f..82997acee3f 100644 --- a/spec/models/packages/package_spec.rb +++ b/spec/models/packages/package_spec.rb @@ -22,6 +22,14 @@ RSpec.describe Packages::Package, type: :model do it { is_expected.to have_one(:rubygems_metadatum).inverse_of(:package) } end + describe '.with_debian_codename' do + let_it_be(:publication) { create(:debian_publication) } + + subject { described_class.with_debian_codename(publication.distribution.codename).to_a } + + it { is_expected.to contain_exactly(publication.package) } + end + describe '.with_composer_target' do let!(:package1) { create(:composer_package, :with_metadatum, sha: '123') } let!(:package2) { create(:composer_package, :with_metadatum, sha: '123') } @@ -162,6 +170,18 @@ RSpec.describe Packages::Package, type: :model do it { is_expected.not_to allow_value('../../../my_package').for(:name) } it { is_expected.not_to allow_value('%2e%2e%2fmy_package').for(:name) } end + + context 'npm package' do + subject { build_stubbed(:npm_package) } + + it { is_expected.to allow_value("@group-1/package").for(:name) } + it { is_expected.to allow_value("@any-scope/package").for(:name) } + it { is_expected.to allow_value("unscoped-package").for(:name) } + it { is_expected.not_to allow_value("@inv@lid-scope/package").for(:name) } + it { is_expected.not_to allow_value("@scope/../../package").for(:name) } + it { is_expected.not_to allow_value("@scope%2e%2e%fpackage").for(:name) } + it { is_expected.not_to allow_value("@scope/sub/package").for(:name) } + end end describe '#version' do @@ -342,16 +362,6 @@ RSpec.describe Packages::Package, type: :model do end describe '#package_already_taken' do - context 'npm package' do - let!(:package) { create(:npm_package) } - - it 'will not allow a package of the same name' do - new_package = build(:npm_package, project: create(:project), name: package.name) - - expect(new_package).not_to be_valid - end - end - context 'maven package' do let!(:package) { create(:maven_package) } @@ -511,7 +521,7 @@ RSpec.describe Packages::Package, type: :model do describe '.without_nuget_temporary_name' do let!(:package1) { create(:nuget_package) } - let!(:package2) { create(:nuget_package, name: Packages::Nuget::CreatePackageService::TEMPORARY_PACKAGE_NAME) } + let!(:package2) { create(:nuget_package, name: Packages::Nuget::TEMPORARY_PACKAGE_NAME) } subject { described_class.without_nuget_temporary_name } @@ -530,7 +540,7 @@ RSpec.describe Packages::Package, type: :model do it { is_expected.to match_array([package1, package2, package3]) } context 'with temporary packages' do - let!(:package1) { create(:nuget_package, name: Packages::Nuget::CreatePackageService::TEMPORARY_PACKAGE_NAME) } + let!(:package1) { create(:nuget_package, name: Packages::Nuget::TEMPORARY_PACKAGE_NAME) } it { is_expected.to match_array([package2, package3]) } end @@ -803,4 +813,63 @@ RSpec.describe Packages::Package, type: :model do expect(package.package_settings).to eq(group.package_settings) end end + + describe '#sync_maven_metadata' do + let_it_be(:user) { create(:user) } + let_it_be(:package) { create(:maven_package) } + + subject { package.sync_maven_metadata(user) } + + shared_examples 'not enqueuing a sync worker job' do + it 'does not enqueue a sync worker job' do + expect(::Packages::Maven::Metadata::SyncWorker) + .not_to receive(:perform_async) + + subject + end + end + + it 'enqueues a sync worker job' do + expect(::Packages::Maven::Metadata::SyncWorker) + .to receive(:perform_async).with(user.id, package.project.id, package.name) + + subject + end + + context 'with no user' do + let(:user) { nil } + + it_behaves_like 'not enqueuing a sync worker job' + end + + context 'with a versionless maven package' do + let_it_be(:package) { create(:maven_package, version: nil) } + + it_behaves_like 'not enqueuing a sync worker job' + end + + context 'with a non maven package' do + let_it_be(:package) { create(:npm_package) } + + it_behaves_like 'not enqueuing a sync worker job' + end + end + + context 'destroying a composer package' do + let_it_be(:package_name) { 'composer-package-name' } + let_it_be(:json) { { 'name' => package_name } } + let_it_be(:project) { create(:project, :custom_repo, files: { 'composer.json' => json.to_json } ) } + let!(:package) { create(:composer_package, :with_metadatum, project: project, name: package_name, version: '1.0.0', json: json) } + + before do + Gitlab::Composer::Cache.new(project: project, name: package_name).execute + package.composer_metadatum.reload + end + + it 'schedule the update job' do + expect(::Packages::Composer::CacheUpdateWorker).to receive(:perform_async).with(project.id, package_name, package.composer_metadatum.version_cache_sha) + + package.destroy! + end + end end diff --git a/spec/models/pages/lookup_path_spec.rb b/spec/models/pages/lookup_path_spec.rb index 0a2b04f1a7c..9e65635da91 100644 --- a/spec/models/pages/lookup_path_spec.rb +++ b/spec/models/pages/lookup_path_spec.rb @@ -117,14 +117,6 @@ RSpec.describe Pages::LookupPath do end end - context 'when pages_serve_from_deployments feature flag is disabled' do - before do - stub_feature_flags(pages_serve_from_deployments: false) - end - - include_examples 'uses disk storage' - end - context 'when deployment were created during migration' do before do allow(deployment).to receive(:migrated?).and_return(true) diff --git a/spec/models/project_feature_spec.rb b/spec/models/project_feature_spec.rb index 37402fa04c4..6d0a5face38 100644 --- a/spec/models/project_feature_spec.rb +++ b/spec/models/project_feature_spec.rb @@ -40,7 +40,7 @@ RSpec.describe ProjectFeature do end context 'public features' do - features = %w(issues wiki builds merge_requests snippets repository metrics_dashboard operations) + features = ProjectFeature::FEATURES - %i(pages) features.each do |feature| it "does not allow public access level for #{feature}" do diff --git a/spec/models/project_repository_storage_move_spec.rb b/spec/models/project_repository_storage_move_spec.rb index 88535f6dd6e..eb193a44680 100644 --- a/spec/models/project_repository_storage_move_spec.rb +++ b/spec/models/project_repository_storage_move_spec.rb @@ -9,7 +9,7 @@ RSpec.describe ProjectRepositoryStorageMove, type: :model do let(:container) { project } let(:repository_storage_factory_key) { :project_repository_storage_move } let(:error_key) { :project } - let(:repository_storage_worker) { ProjectUpdateRepositoryStorageWorker } + let(:repository_storage_worker) { Projects::UpdateRepositoryStorageWorker } end describe 'state transitions' do diff --git a/spec/models/project_services/discord_service_spec.rb b/spec/models/project_services/discord_service_spec.rb index d4bd08ddeb6..ffe0a36dcdc 100644 --- a/spec/models/project_services/discord_service_spec.rb +++ b/spec/models/project_services/discord_service_spec.rb @@ -6,7 +6,16 @@ RSpec.describe DiscordService do it_behaves_like "chat service", "Discord notifications" do let(:client) { Discordrb::Webhooks::Client } let(:client_arguments) { { url: webhook_url } } - let(:content_key) { :content } + let(:payload) do + { + embeds: [ + include( + author: include(name: be_present), + description: be_present + ) + ] + } + end end describe '#execute' do @@ -58,5 +67,16 @@ RSpec.describe DiscordService do expect { subject.execute(sample_data) }.to raise_error(ArgumentError, /is blocked/) end end + + context 'when the Discord request fails' do + before do + WebMock.stub_request(:post, webhook_url).to_return(status: 400) + end + + it 'logs an error and returns false' do + expect(subject).to receive(:log_error).with('400 Bad Request') + expect(subject.execute(sample_data)).to be(false) + end + end end end diff --git a/spec/models/project_services/hangouts_chat_service_spec.rb b/spec/models/project_services/hangouts_chat_service_spec.rb index 042e32439d1..9d3bd457fc8 100644 --- a/spec/models/project_services/hangouts_chat_service_spec.rb +++ b/spec/models/project_services/hangouts_chat_service_spec.rb @@ -6,6 +6,10 @@ RSpec.describe HangoutsChatService do it_behaves_like "chat service", "Hangouts Chat" do let(:client) { HangoutsChat::Sender } let(:client_arguments) { webhook_url } - let(:content_key) { :text } + let(:payload) do + { + text: be_present + } + end end end diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb index 78bd0e91208..3fc39fd3266 100644 --- a/spec/models/project_services/jira_service_spec.rb +++ b/spec/models/project_services/jira_service_spec.rb @@ -474,21 +474,32 @@ RSpec.describe JiraService do let(:custom_base_url) { 'http://custom_url' } shared_examples 'close_issue' do + let(:issue_key) { 'JIRA-123' } + let(:issue_url) { "#{url}/rest/api/2/issue/#{issue_key}" } + let(:transitions_url) { "#{issue_url}/transitions" } + let(:comment_url) { "#{issue_url}/comment" } + let(:remote_link_url) { "#{issue_url}/remotelink" } + let(:transitions) { nil } + + let(:issue_fields) do + { + id: issue_key, + self: issue_url, + transitions: transitions + } + end + + subject(:close_issue) do + jira_service.close_issue(resource, ExternalIssue.new(issue_key, project)) + end + before do - @jira_service = described_class.new - allow(@jira_service).to receive_messages( - project_id: project.id, - project: project, - url: 'http://jira.example.com', - username: 'gitlab_jira_username', - password: 'gitlab_jira_password', - jira_issue_transition_id: '999' - ) + allow(jira_service).to receive_messages(jira_issue_transition_id: '999') # These stubs are needed to test JiraService#close_issue. # We close the issue then do another request to API to check if it got closed. # Here is stubbed the API return with a closed and an opened issues. - open_issue = JIRA::Resource::Issue.new(@jira_service.client, attrs: { 'id' => 'JIRA-123' }) + open_issue = JIRA::Resource::Issue.new(jira_service.client, attrs: issue_fields.deep_stringify_keys) closed_issue = open_issue.dup allow(open_issue).to receive(:resolution).and_return(false) allow(closed_issue).to receive(:resolution).and_return(true) @@ -497,29 +508,22 @@ RSpec.describe JiraService do allow_any_instance_of(JIRA::Resource::Issue).to receive(:key).and_return('JIRA-123') allow(JIRA::Resource::Remotelink).to receive(:all).and_return([]) - @jira_service.save! - - project_issues_url = 'http://jira.example.com/rest/api/2/issue/JIRA-123' - @transitions_url = 'http://jira.example.com/rest/api/2/issue/JIRA-123/transitions' - @comment_url = 'http://jira.example.com/rest/api/2/issue/JIRA-123/comment' - @remote_link_url = 'http://jira.example.com/rest/api/2/issue/JIRA-123/remotelink' - - WebMock.stub_request(:get, project_issues_url).with(basic_auth: %w(gitlab_jira_username gitlab_jira_password)) - WebMock.stub_request(:post, @transitions_url).with(basic_auth: %w(gitlab_jira_username gitlab_jira_password)) - WebMock.stub_request(:post, @comment_url).with(basic_auth: %w(gitlab_jira_username gitlab_jira_password)) - WebMock.stub_request(:post, @remote_link_url).with(basic_auth: %w(gitlab_jira_username gitlab_jira_password)) + WebMock.stub_request(:get, issue_url).with(basic_auth: %w(jira-username jira-password)) + WebMock.stub_request(:post, transitions_url).with(basic_auth: %w(jira-username jira-password)) + WebMock.stub_request(:post, comment_url).with(basic_auth: %w(jira-username jira-password)) + WebMock.stub_request(:post, remote_link_url).with(basic_auth: %w(jira-username jira-password)) end let(:external_issue) { ExternalIssue.new('JIRA-123', project) } def close_issue - @jira_service.close_issue(resource, external_issue, current_user) + jira_service.close_issue(resource, external_issue, current_user) end it 'calls Jira API' do close_issue - expect(WebMock).to have_requested(:post, @comment_url).with( + expect(WebMock).to have_requested(:post, comment_url).with( body: /Issue solved with/ ).once end @@ -546,9 +550,9 @@ RSpec.describe JiraService do favicon_path = "http://localhost/assets/#{find_asset('favicon.png').digest_path}" # Creates comment - expect(WebMock).to have_requested(:post, @comment_url) + expect(WebMock).to have_requested(:post, comment_url) # Creates Remote Link in Jira issue fields - expect(WebMock).to have_requested(:post, @remote_link_url).with( + expect(WebMock).to have_requested(:post, remote_link_url).with( body: hash_including( GlobalID: 'GitLab', relationship: 'mentioned on', @@ -564,11 +568,11 @@ RSpec.describe JiraService do context 'when "comment_on_event_enabled" is set to false' do it 'creates Remote Link reference but does not create comment' do - allow(@jira_service).to receive_messages(comment_on_event_enabled: false) + allow(jira_service).to receive_messages(comment_on_event_enabled: false) close_issue - expect(WebMock).not_to have_requested(:post, @comment_url) - expect(WebMock).to have_requested(:post, @remote_link_url) + expect(WebMock).not_to have_requested(:post, comment_url) + expect(WebMock).to have_requested(:post, remote_link_url) end end @@ -589,7 +593,7 @@ RSpec.describe JiraService do close_issue - expect(WebMock).not_to have_requested(:post, @comment_url) + expect(WebMock).not_to have_requested(:post, comment_url) end end @@ -598,8 +602,8 @@ RSpec.describe JiraService do close_issue - expect(WebMock).not_to have_requested(:post, @comment_url) - expect(WebMock).not_to have_requested(:post, @remote_link_url) + expect(WebMock).not_to have_requested(:post, comment_url) + expect(WebMock).not_to have_requested(:post, remote_link_url) end it 'does not send comment or remote links to issues with unknown resolution' do @@ -607,8 +611,8 @@ RSpec.describe JiraService do close_issue - expect(WebMock).not_to have_requested(:post, @comment_url) - expect(WebMock).not_to have_requested(:post, @remote_link_url) + expect(WebMock).not_to have_requested(:post, comment_url) + expect(WebMock).not_to have_requested(:post, remote_link_url) end it 'references the GitLab commit' do @@ -616,7 +620,7 @@ RSpec.describe JiraService do close_issue - expect(WebMock).to have_requested(:post, @comment_url).with( + expect(WebMock).to have_requested(:post, comment_url).with( body: %r{#{custom_base_url}/#{project.full_path}/-/commit/#{commit_id}} ).once end @@ -631,18 +635,18 @@ RSpec.describe JiraService do close_issue - expect(WebMock).to have_requested(:post, @comment_url).with( + expect(WebMock).to have_requested(:post, comment_url).with( body: %r{#{Gitlab.config.gitlab.url}/#{project.full_path}/-/commit/#{commit_id}} ).once end it 'logs exception when transition id is not valid' do - allow(@jira_service).to receive(:log_error) - WebMock.stub_request(:post, @transitions_url).with(basic_auth: %w(gitlab_jira_username gitlab_jira_password)).and_raise("Bad Request") + allow(jira_service).to receive(:log_error) + WebMock.stub_request(:post, transitions_url).with(basic_auth: %w(jira-username jira-password)).and_raise("Bad Request") close_issue - expect(@jira_service).to have_received(:log_error).with( + expect(jira_service).to have_received(:log_error).with( "Issue transition failed", error: hash_including( exception_class: 'StandardError', @@ -655,34 +659,64 @@ RSpec.describe JiraService do it 'calls the api with jira_issue_transition_id' do close_issue - expect(WebMock).to have_requested(:post, @transitions_url).with( - body: /999/ + expect(WebMock).to have_requested(:post, transitions_url).with( + body: /"id":"999"/ ).once end - context 'when have multiple transition ids' do - it 'calls the api with transition ids separated by comma' do - allow(@jira_service).to receive_messages(jira_issue_transition_id: '1,2,3') + context 'when using multiple transition ids' do + before do + allow(jira_service).to receive_messages(jira_issue_transition_id: '1,2,3') + end + it 'calls the api with transition ids separated by comma' do close_issue 1.upto(3) do |transition_id| - expect(WebMock).to have_requested(:post, @transitions_url).with( - body: /#{transition_id}/ + expect(WebMock).to have_requested(:post, transitions_url).with( + body: /"id":"#{transition_id}"/ ).once end + + expect(WebMock).to have_requested(:post, comment_url) end it 'calls the api with transition ids separated by semicolon' do - allow(@jira_service).to receive_messages(jira_issue_transition_id: '1;2;3') + allow(jira_service).to receive_messages(jira_issue_transition_id: '1;2;3') close_issue 1.upto(3) do |transition_id| - expect(WebMock).to have_requested(:post, @transitions_url).with( - body: /#{transition_id}/ + expect(WebMock).to have_requested(:post, transitions_url).with( + body: /"id":"#{transition_id}"/ ).once end + + expect(WebMock).to have_requested(:post, comment_url) + end + + context 'when a transition fails' do + before do + WebMock.stub_request(:post, transitions_url).with(basic_auth: %w(jira-username jira-password)).to_return do |request| + { status: request.body.include?('"id":"2"') ? 500 : 200 } + end + end + + it 'stops the sequence' do + close_issue + + 1.upto(2) do |transition_id| + expect(WebMock).to have_requested(:post, transitions_url).with( + body: /"id":"#{transition_id}"/ + ) + end + + expect(WebMock).not_to have_requested(:post, transitions_url).with( + body: /"id":"3"/ + ) + + expect(WebMock).not_to have_requested(:post, comment_url) + end end end end diff --git a/spec/models/project_services/prometheus_service_spec.rb b/spec/models/project_services/prometheus_service_spec.rb index ea63406e615..366c3f68e1d 100644 --- a/spec/models/project_services/prometheus_service_spec.rb +++ b/spec/models/project_services/prometheus_service_spec.rb @@ -511,20 +511,23 @@ RSpec.describe PrometheusService, :use_clean_rails_memory_store_caching, :snowpl type: 'checkbox', name: 'manual_configuration', title: s_('PrometheusService|Active'), + help: s_('PrometheusService|Select this checkbox to override the auto configuration settings with your own settings.'), required: true }, { type: 'text', name: 'api_url', title: 'API URL', - placeholder: s_('PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/'), + placeholder: s_('PrometheusService|https://prometheus.example.com/'), + help: s_('PrometheusService|The Prometheus API base URL.'), required: true }, { type: 'text', name: 'google_iap_audience_client_id', title: 'Google IAP Audience Client ID', - placeholder: s_('PrometheusService|Client ID of the IAP secured resource (looks like IAP_CLIENT_ID.apps.googleusercontent.com)'), + placeholder: s_('PrometheusService|IAP_CLIENT_ID.apps.googleusercontent.com'), + help: s_('PrometheusService|PrometheusService|The ID of the IAP-secured resource.'), autocomplete: 'off', required: false }, @@ -532,7 +535,8 @@ RSpec.describe PrometheusService, :use_clean_rails_memory_store_caching, :snowpl type: 'textarea', name: 'google_iap_service_account_json', title: 'Google IAP Service Account JSON', - placeholder: s_('PrometheusService|Contents of the credentials.json file of your service account, like: { "type": "service_account", "project_id": ... }'), + placeholder: s_('PrometheusService|{ "type": "service_account", "project_id": ... }'), + help: s_('PrometheusService|The contents of the credentials.json file of your service account.'), required: false } ] diff --git a/spec/models/project_services/slack_service_spec.rb b/spec/models/project_services/slack_service_spec.rb index 0b35b9e7b30..aa5d92e5c61 100644 --- a/spec/models/project_services/slack_service_spec.rb +++ b/spec/models/project_services/slack_service_spec.rb @@ -4,4 +4,116 @@ require 'spec_helper' RSpec.describe SlackService do it_behaves_like "slack or mattermost notifications", 'Slack' + + describe '#execute' do + before do + stub_request(:post, "https://slack.service.url/") + end + + let_it_be(:slack_service) { create(:slack_service, branches_to_be_notified: 'all') } + + it 'uses only known events', :aggregate_failures do + described_class::SUPPORTED_EVENTS_FOR_USAGE_LOG.each do |action| + expect(Gitlab::UsageDataCounters::HLLRedisCounter.known_event?("i_ecosystem_slack_service_#{action}_notification")).to be true + end + end + + context 'hook data includes a user object' do + let_it_be(:user) { create_default(:user) } + let_it_be(:project) { create_default(:project, :repository, :wiki_repo) } + + shared_examples 'increases the usage data counter' do |event_name| + it 'increases the usage data counter' do + expect(Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:track_event).with(event_name, values: user.id).and_call_original + + slack_service.execute(data) + end + end + + context 'event is not supported for usage log' do + let_it_be(:pipeline) { create(:ci_pipeline) } + let(:data) { Gitlab::DataBuilder::Pipeline.build(pipeline) } + + it 'does not increase the usage data counter' do + expect(Gitlab::UsageDataCounters::HLLRedisCounter).not_to receive(:track_event).with('i_ecosystem_slack_service_pipeline_notification', values: user.id) + + slack_service.execute(data) + end + end + + context 'issue notification' do + let_it_be(:issue) { create(:issue) } + let(:data) { issue.to_hook_data(user) } + + it_behaves_like 'increases the usage data counter', 'i_ecosystem_slack_service_issue_notification' + end + + context 'push notification' do + let(:data) { Gitlab::DataBuilder::Push.build_sample(project, user) } + + it_behaves_like 'increases the usage data counter', 'i_ecosystem_slack_service_push_notification' + end + + context 'deployment notification' do + let_it_be(:deployment) { create(:deployment, user: user) } + let(:data) { Gitlab::DataBuilder::Deployment.build(deployment) } + + it_behaves_like 'increases the usage data counter', 'i_ecosystem_slack_service_deployment_notification' + end + + context 'wiki_page notification' do + let_it_be(:wiki_page) { create(:wiki_page, wiki: project.wiki, message: 'user created page: Awesome wiki_page') } + let(:data) { Gitlab::DataBuilder::WikiPage.build(wiki_page, user, 'create') } + + it_behaves_like 'increases the usage data counter', 'i_ecosystem_slack_service_wiki_page_notification' + end + + context 'merge_request notification' do + let_it_be(:merge_request) { create(:merge_request) } + let(:data) { merge_request.to_hook_data(user) } + + it_behaves_like 'increases the usage data counter', 'i_ecosystem_slack_service_merge_request_notification' + end + + context 'note notification' do + let_it_be(:issue_note) { create(:note_on_issue, note: 'issue note') } + let(:data) { Gitlab::DataBuilder::Note.build(issue_note, user) } + + it_behaves_like 'increases the usage data counter', 'i_ecosystem_slack_service_note_notification' + end + + context 'tag_push notification' do + let(:oldrev) { Gitlab::Git::BLANK_SHA } + let(:newrev) { '8a2a6eb295bb170b34c24c76c49ed0e9b2eaf34b' } # gitlab-test: git rev-parse refs/tags/v1.1.0 + let(:ref) { 'refs/tags/v1.1.0' } + let(:data) { Git::TagHooksService.new(project, user, change: { oldrev: oldrev, newrev: newrev, ref: ref }).send(:push_data) } + + it_behaves_like 'increases the usage data counter', 'i_ecosystem_slack_service_tag_push_notification' + end + + context 'confidential note notification' do + let_it_be(:confidential_issue_note) { create(:note_on_issue, note: 'issue note', confidential: true) } + let(:data) { Gitlab::DataBuilder::Note.build(confidential_issue_note, user) } + + it_behaves_like 'increases the usage data counter', 'i_ecosystem_slack_service_confidential_note_notification' + end + + context 'confidential issue notification' do + let_it_be(:issue) { create(:issue, confidential: true) } + let(:data) { issue.to_hook_data(user) } + + it_behaves_like 'increases the usage data counter', 'i_ecosystem_slack_service_confidential_issue_notification' + end + end + + context 'hook data does not include a user' do + let(:data) { Gitlab::DataBuilder::Pipeline.build(create(:ci_pipeline)) } + + it 'does not increase the usage data counter' do + expect(Gitlab::UsageDataCounters::HLLRedisCounter).not_to receive(:track_event) + + slack_service.execute(data) + end + end + end end diff --git a/spec/models/project_services/unify_circuit_service_spec.rb b/spec/models/project_services/unify_circuit_service_spec.rb index 73702aa8471..0c749322e07 100644 --- a/spec/models/project_services/unify_circuit_service_spec.rb +++ b/spec/models/project_services/unify_circuit_service_spec.rb @@ -5,6 +5,12 @@ require "spec_helper" RSpec.describe UnifyCircuitService do it_behaves_like "chat service", "Unify Circuit" do let(:client_arguments) { webhook_url } - let(:content_key) { :subject } + let(:payload) do + { + subject: project.full_name, + text: be_present, + markdown: true + } + end end end diff --git a/spec/models/project_services/webex_teams_service_spec.rb b/spec/models/project_services/webex_teams_service_spec.rb index bd73d0c93b8..ed63f5bc48c 100644 --- a/spec/models/project_services/webex_teams_service_spec.rb +++ b/spec/models/project_services/webex_teams_service_spec.rb @@ -5,6 +5,10 @@ require "spec_helper" RSpec.describe WebexTeamsService do it_behaves_like "chat service", "Webex Teams" do let(:client_arguments) { webhook_url } - let(:content_key) { :markdown } + let(:payload) do + { + markdown: be_present + } + end end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index fd7975bf65d..b4010ba6e31 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -8,7 +8,7 @@ RSpec.describe Project, factory_default: :keep do include ExternalAuthorizationServiceHelpers using RSpec::Parameterized::TableSyntax - let_it_be(:namespace) { create_default(:namespace) } + let_it_be(:namespace) { create_default(:namespace).freeze } it_behaves_like 'having unique enum values' @@ -145,7 +145,7 @@ RSpec.describe Project, factory_default: :keep do end it_behaves_like 'model with wiki' do - let_it_be(:container) { create(:project, :wiki_repo) } + let_it_be(:container) { create(:project, :wiki_repo, namespace: create(:group)) } let(:container_without_wiki) { create(:project) } end @@ -1599,7 +1599,7 @@ RSpec.describe Project, factory_default: :keep do end end - describe '#any_runners?' do + describe '#any_active_runners?' do context 'shared runners' do let(:project) { create(:project, shared_runners_enabled: shared_runners_enabled) } let(:specific_runner) { create(:ci_runner, :project, projects: [project]) } @@ -1609,31 +1609,31 @@ RSpec.describe Project, factory_default: :keep do let(:shared_runners_enabled) { false } it 'has no runners available' do - expect(project.any_runners?).to be_falsey + expect(project.any_active_runners?).to be_falsey end it 'has a specific runner' do specific_runner - expect(project.any_runners?).to be_truthy + expect(project.any_active_runners?).to be_truthy end it 'has a shared runner, but they are prohibited to use' do shared_runner - expect(project.any_runners?).to be_falsey + expect(project.any_active_runners?).to be_falsey end it 'checks the presence of specific runner' do specific_runner - expect(project.any_runners? { |runner| runner == specific_runner }).to be_truthy + expect(project.any_active_runners? { |runner| runner == specific_runner }).to be_truthy end it 'returns false if match cannot be found' do specific_runner - expect(project.any_runners? { false }).to be_falsey + expect(project.any_active_runners? { false }).to be_falsey end end @@ -1643,19 +1643,19 @@ RSpec.describe Project, factory_default: :keep do it 'has a shared runner' do shared_runner - expect(project.any_runners?).to be_truthy + expect(project.any_active_runners?).to be_truthy end it 'checks the presence of shared runner' do shared_runner - expect(project.any_runners? { |runner| runner == shared_runner }).to be_truthy + expect(project.any_active_runners? { |runner| runner == shared_runner }).to be_truthy end it 'returns false if match cannot be found' do shared_runner - expect(project.any_runners? { false }).to be_falsey + expect(project.any_active_runners? { false }).to be_falsey end end end @@ -1669,13 +1669,13 @@ RSpec.describe Project, factory_default: :keep do let(:group_runners_enabled) { false } it 'has no runners available' do - expect(project.any_runners?).to be_falsey + expect(project.any_active_runners?).to be_falsey end it 'has a group runner, but they are prohibited to use' do group_runner - expect(project.any_runners?).to be_falsey + expect(project.any_active_runners?).to be_falsey end end @@ -1685,19 +1685,19 @@ RSpec.describe Project, factory_default: :keep do it 'has a group runner' do group_runner - expect(project.any_runners?).to be_truthy + expect(project.any_active_runners?).to be_truthy end it 'checks the presence of group runner' do group_runner - expect(project.any_runners? { |runner| runner == group_runner }).to be_truthy + expect(project.any_active_runners? { |runner| runner == group_runner }).to be_truthy end it 'returns false if match cannot be found' do group_runner - expect(project.any_runners? { false }).to be_falsey + expect(project.any_active_runners? { false }).to be_falsey end end end @@ -1799,7 +1799,8 @@ RSpec.describe Project, factory_default: :keep do describe '#default_branch_protected?' do using RSpec::Parameterized::TableSyntax - let_it_be(:project) { create(:project) } + let_it_be(:namespace) { create(:namespace) } + let_it_be(:project) { create(:project, namespace: namespace) } subject { project.default_branch_protected? } @@ -2802,7 +2803,8 @@ RSpec.describe Project, factory_default: :keep do end describe '#emails_disabled?' do - let(:project) { build(:project, emails_disabled: false) } + let_it_be(:namespace) { create(:namespace) } + let(:project) { build(:project, namespace: namespace, emails_disabled: false) } context 'emails disabled in group' do it 'returns true' do @@ -2830,7 +2832,8 @@ RSpec.describe Project, factory_default: :keep do end describe '#lfs_enabled?' do - let(:project) { build(:project) } + let(:namespace) { create(:namespace) } + let(:project) { build(:project, namespace: namespace) } shared_examples 'project overrides group' do it 'returns true when enabled in project' do @@ -4463,7 +4466,11 @@ RSpec.describe Project, factory_default: :keep do subject { project.predefined_project_variables.to_runner_variables } specify do - expect(subject).to include({ key: 'CI_PROJECT_CONFIG_PATH', value: Ci::Pipeline::DEFAULT_CONFIG_PATH, public: true, masked: false }) + expect(subject).to include + [ + { key: 'CI_PROJECT_CONFIG_PATH', value: Ci::Pipeline::DEFAULT_CONFIG_PATH, public: true, masked: false }, + { key: 'CI_CONFIG_PATH', value: Ci::Pipeline::DEFAULT_CONFIG_PATH, public: true, masked: false } + ] end context 'when ci config path is overridden' do @@ -4471,7 +4478,13 @@ RSpec.describe Project, factory_default: :keep do project.update!(ci_config_path: 'random.yml') end - it { expect(subject).to include({ key: 'CI_PROJECT_CONFIG_PATH', value: 'random.yml', public: true, masked: false }) } + it do + expect(subject).to include + [ + { key: 'CI_PROJECT_CONFIG_PATH', value: 'random.yml', public: true, masked: false }, + { key: 'CI_CONFIG_PATH', value: 'random.yml', public: true, masked: false } + ] + end end end @@ -4877,7 +4890,8 @@ RSpec.describe Project, factory_default: :keep do end context 'branch protection' do - let(:project) { create(:project, :repository) } + let_it_be(:namespace) { create(:namespace) } + let(:project) { create(:project, :repository, namespace: namespace) } before do create(:import_state, :started, project: project) diff --git a/spec/models/projects/repository_storage_move_spec.rb b/spec/models/projects/repository_storage_move_spec.rb new file mode 100644 index 00000000000..ab0ad81f77a --- /dev/null +++ b/spec/models/projects/repository_storage_move_spec.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Projects::RepositoryStorageMove, type: :model do + let_it_be_with_refind(:project) { create(:project) } + + it_behaves_like 'handles repository moves' do + let(:container) { project } + let(:repository_storage_factory_key) { :project_repository_storage_move } + let(:error_key) { :project } + let(:repository_storage_worker) { Projects::UpdateRepositoryStorageWorker } + end + + describe 'state transitions' do + let(:storage) { 'test_second_storage' } + + before do + stub_storage_settings(storage => { 'path' => 'tmp/tests/extra_storage' }) + end + + context 'when started' do + subject(:storage_move) { create(:project_repository_storage_move, :started, container: project, destination_storage_name: storage) } + + context 'and transits to replicated' do + it 'sets the repository storage and marks the container as writable' do + storage_move.finish_replication! + + expect(project.repository_storage).to eq(storage) + expect(project).not_to be_repository_read_only + end + end + end + end +end diff --git a/spec/models/prometheus_alert_event_spec.rb b/spec/models/prometheus_alert_event_spec.rb index 913ca7db0be..6bff549bc4b 100644 --- a/spec/models/prometheus_alert_event_spec.rb +++ b/spec/models/prometheus_alert_event_spec.rb @@ -52,7 +52,7 @@ RSpec.describe PrometheusAlertEvent do let(:started_at) { Time.current } context 'when status is none' do - subject { build(:prometheus_alert_event, :none) } + subject { build(:prometheus_alert_event, status: nil, started_at: nil) } it 'fires an event' do result = subject.fire(started_at) diff --git a/spec/models/protected_branch_spec.rb b/spec/models/protected_branch_spec.rb index a89f8778780..a173ab48f17 100644 --- a/spec/models/protected_branch_spec.rb +++ b/spec/models/protected_branch_spec.rb @@ -207,6 +207,28 @@ RSpec.describe ProtectedBranch do end 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") } + + it "returns true" do + project = subject_branch.project + + expect(described_class.allow_force_push?(project, "foo")).to eq(true) + end + end + + context "when the attr allow_force_push is false" do + let(:subject_branch) { create(:protected_branch, allow_force_push: false, name: "foo") } + + it "returns false" do + project = subject_branch.project + + expect(described_class.allow_force_push?(project, "foo")).to eq(false) + end + end + end + describe '#any_protected?' do context 'existing project' do let(:project) { create(:project, :repository) } diff --git a/spec/models/snippet_repository_spec.rb b/spec/models/snippet_repository_spec.rb index cdbc1feefce..11196f06529 100644 --- a/spec/models/snippet_repository_spec.rb +++ b/spec/models/snippet_repository_spec.rb @@ -286,6 +286,7 @@ RSpec.describe SnippetRepository do context 'with git errors' do it_behaves_like 'snippet repository with git errors', 'invalid://path/here', described_class::InvalidPathError + it_behaves_like 'snippet repository with git errors', '.git/hooks/pre-commit', described_class::InvalidPathError it_behaves_like 'snippet repository with git errors', '../../path/traversal/here', described_class::InvalidPathError it_behaves_like 'snippet repository with git errors', 'README', described_class::CommitError diff --git a/spec/models/snippet_repository_storage_move_spec.rb b/spec/models/snippet_repository_storage_move_spec.rb index 357951f8859..f5ad837fb36 100644 --- a/spec/models/snippet_repository_storage_move_spec.rb +++ b/spec/models/snippet_repository_storage_move_spec.rb @@ -8,6 +8,6 @@ RSpec.describe SnippetRepositoryStorageMove, type: :model do let(:repository_storage_factory_key) { :snippet_repository_storage_move } let(:error_key) { :snippet } - let(:repository_storage_worker) { SnippetUpdateRepositoryStorageWorker } + let(:repository_storage_worker) { Snippets::UpdateRepositoryStorageWorker } end end diff --git a/spec/models/snippet_spec.rb b/spec/models/snippet_spec.rb index 623767d19e0..09f9cf8e222 100644 --- a/spec/models/snippet_spec.rb +++ b/spec/models/snippet_spec.rb @@ -21,7 +21,7 @@ RSpec.describe Snippet do it { is_expected.to have_many(:user_mentions).class_name("SnippetUserMention") } it { is_expected.to have_one(:snippet_repository) } it { is_expected.to have_one(:statistics).class_name('SnippetStatistics').dependent(:destroy) } - it { is_expected.to have_many(:repository_storage_moves).class_name('SnippetRepositoryStorageMove').inverse_of(:container) } + it { is_expected.to have_many(:repository_storage_moves).class_name('Snippets::RepositoryStorageMove').inverse_of(:container) } end describe 'validation' do @@ -496,6 +496,16 @@ RSpec.describe Snippet do it 'returns array of blobs' do expect(snippet.blobs).to all(be_a(Blob)) end + + context 'when file does not exist' do + it 'removes nil values from the blobs array' do + allow(snippet).to receive(:list_files).and_return(%w(LICENSE non_existent_snippet_file)) + + blobs = snippet.blobs + expect(blobs.count).to eq 1 + expect(blobs.first.name).to eq 'LICENSE' + end + end end end diff --git a/spec/models/snippets/repository_storage_move_spec.rb b/spec/models/snippets/repository_storage_move_spec.rb new file mode 100644 index 00000000000..ed518faf6ff --- /dev/null +++ b/spec/models/snippets/repository_storage_move_spec.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Snippets::RepositoryStorageMove, type: :model do + it_behaves_like 'handles repository moves' do + let_it_be_with_refind(:container) { create(:snippet) } + + let(:repository_storage_factory_key) { :snippet_repository_storage_move } + let(:error_key) { :snippet } + let(:repository_storage_worker) { Snippets::UpdateRepositoryStorageWorker } + end +end diff --git a/spec/models/todo_spec.rb b/spec/models/todo_spec.rb index a9c4c6680cd..855b1b0f3f7 100644 --- a/spec/models/todo_spec.rb +++ b/spec/models/todo_spec.rb @@ -363,23 +363,6 @@ RSpec.describe Todo do end end - describe '.for_ids' do - it 'returns the expected todos' do - todo1 = create(:todo) - todo2 = create(:todo) - todo3 = create(:todo) - create(:todo) - - expect(described_class.for_ids([todo2.id, todo1.id, todo3.id])).to contain_exactly(todo1, todo2, todo3) - end - - it 'returns an empty collection when no ids are given' do - create(:todo) - - expect(described_class.for_ids([])).to be_empty - end - end - describe '.for_user' do it 'returns the expected todos' do user1 = create(:user) diff --git a/spec/models/upload_spec.rb b/spec/models/upload_spec.rb index 18388b4cd83..6bac5e31435 100644 --- a/spec/models/upload_spec.rb +++ b/spec/models/upload_spec.rb @@ -221,7 +221,7 @@ RSpec.describe Upload do it 'does not send a message to Sentry' do upload = described_class.new(path: "#{__FILE__}-nope", store: ObjectStorage::Store::LOCAL) - expect(Raven).not_to receive(:capture_message) + expect(Gitlab::ErrorTracking).not_to receive(:track_exception) upload.exist? end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 860c015e166..5f2842c9d16 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -41,6 +41,9 @@ RSpec.describe User do it { is_expected.to delegate_method(:show_whitespace_in_diffs).to(:user_preference) } it { is_expected.to delegate_method(:show_whitespace_in_diffs=).to(:user_preference).with_arguments(:args) } + it { is_expected.to delegate_method(:view_diffs_file_by_file).to(:user_preference) } + it { is_expected.to delegate_method(:view_diffs_file_by_file=).to(:user_preference).with_arguments(:args) } + it { is_expected.to delegate_method(:tab_width).to(:user_preference) } it { is_expected.to delegate_method(:tab_width=).to(:user_preference).with_arguments(:args) } @@ -59,6 +62,9 @@ RSpec.describe User do it { is_expected.to delegate_method(:experience_level).to(:user_preference) } it { is_expected.to delegate_method(:experience_level=).to(:user_preference).with_arguments(:args) } + it { is_expected.to delegate_method(:markdown_surround_selection).to(:user_preference) } + it { is_expected.to delegate_method(:markdown_surround_selection=).to(:user_preference).with_arguments(:args) } + it { is_expected.to delegate_method(:job_title).to(:user_detail).allow_nil } it { is_expected.to delegate_method(:job_title=).to(:user_detail).with_arguments(:args).allow_nil } @@ -101,6 +107,7 @@ RSpec.describe User do it { is_expected.to have_many(:reviews).inverse_of(:author) } it { is_expected.to have_many(:merge_request_assignees).inverse_of(:assignee) } it { is_expected.to have_many(:merge_request_reviewers).inverse_of(:reviewer) } + it { is_expected.to have_many(:created_custom_emoji).inverse_of(:creator) } describe "#user_detail" do it 'does not persist `user_detail` by default' do @@ -380,11 +387,11 @@ RSpec.describe User do it { is_expected.not_to allow_value(-1).for(:projects_limit) } it { is_expected.not_to allow_value(Gitlab::Database::MAX_INT_VALUE + 1).for(:projects_limit) } - it_behaves_like 'an object with email-formated attributes', :email do + it_behaves_like 'an object with email-formatted attributes', :email do subject { build(:user) } end - it_behaves_like 'an object with RFC3696 compliant email-formated attributes', :public_email, :notification_email do + it_behaves_like 'an object with RFC3696 compliant email-formatted attributes', :public_email, :notification_email do subject { create(:user).tap { |user| user.emails << build(:email, email: email_value, confirmed_at: Time.current) } } end @@ -1050,7 +1057,7 @@ RSpec.describe User do let(:user) { create(:user) } let(:external_user) { create(:user, external: true) } - it "sets other properties aswell" do + it "sets other properties as well" do expect(external_user.can_create_team).to be_falsey expect(external_user.can_create_group).to be_falsey expect(external_user.projects_limit).to be 0 @@ -1061,7 +1068,7 @@ RSpec.describe User do let(:user) { create(:user) } let(:secondary) { create(:email, :confirmed, email: 'secondary@example.com', user: user) } - it 'allows a verfied secondary email to be used as the primary without needing reconfirmation' do + it 'allows a verified secondary email to be used as the primary without needing reconfirmation' do user.update!(email: secondary.email) user.reload expect(user.email).to eq secondary.email @@ -1827,7 +1834,7 @@ RSpec.describe User do end describe '.instance_access_request_approvers_to_be_notified' do - let_it_be(:admin_list) { create_list(:user, 12, :admin, :with_sign_ins) } + let_it_be(:admin_issue_board_list) { create_list(:user, 12, :admin, :with_sign_ins) } it 'returns up to the ten most recently active instance admins' do active_admins_in_recent_sign_in_desc_order = User.admins.active.order_recent_sign_in.limit(10) @@ -2492,6 +2499,38 @@ RSpec.describe User do end end + describe "#clear_avatar_caches" do + let(:user) { create(:user) } + + context "when :avatar_cache_for_email flag is enabled" do + before do + stub_feature_flags(avatar_cache_for_email: true) + end + + it "clears the avatar cache when saving" do + allow(user).to receive(:avatar_changed?).and_return(true) + + expect(Gitlab::AvatarCache).to receive(:delete_by_email).with(*user.verified_emails) + + user.update(avatar: fixture_file_upload('spec/fixtures/dk.png')) + end + end + + context "when :avatar_cache_for_email flag is disabled" do + before do + stub_feature_flags(avatar_cache_for_email: false) + end + + it "doesn't attempt to clear the avatar cache" do + allow(user).to receive(:avatar_changed?).and_return(true) + + expect(Gitlab::AvatarCache).not_to receive(:delete_by_email) + + user.update(avatar: fixture_file_upload('spec/fixtures/dk.png')) + end + end + end + describe '#accept_pending_invitations!' do let(:user) { create(:user, email: 'user@email.com') } let!(:project_member_invite) { create(:project_member, :invited, invite_email: user.email) } @@ -3227,23 +3266,8 @@ RSpec.describe User do create(:group_group_link, shared_group: private_group, shared_with_group: other_group) end - context 'when shared_group_membership_auth is enabled' do - before do - stub_feature_flags(shared_group_membership_auth: user) - end - - it { is_expected.to include shared_group } - it { is_expected.not_to include other_group } - end - - context 'when shared_group_membership_auth is disabled' do - before do - stub_feature_flags(shared_group_membership_auth: false) - end - - it { is_expected.not_to include shared_group } - it { is_expected.not_to include other_group } - end + it { is_expected.to include shared_group } + it { is_expected.not_to include other_group } end end @@ -3937,6 +3961,37 @@ RSpec.describe User do end end + describe '#can_admin_all_resources?', :request_store do + it 'returns false for regular user' do + user = build_stubbed(:user) + + expect(user.can_admin_all_resources?).to be_falsy + end + + context 'for admin user' do + include_context 'custom session' + + let(:user) { build_stubbed(:user, :admin) } + + context 'when admin mode is disabled' do + it 'returns false' do + expect(user.can_admin_all_resources?).to be_falsy + end + end + + context 'when admin mode is enabled' do + before do + Gitlab::Auth::CurrentUserMode.new(user).request_admin_mode! + Gitlab::Auth::CurrentUserMode.new(user).enable_admin_mode!(password: user.password) + end + + it 'returns true' do + expect(user.can_admin_all_resources?).to be_truthy + end + end + end + end + describe '.ghost' do it "creates a ghost user if one isn't already present" do ghost = described_class.ghost @@ -5370,6 +5425,40 @@ RSpec.describe User do end end + describe 'can_trigger_notifications?' do + context 'when user is not confirmed' do + let_it_be(:user) { create(:user, :unconfirmed) } + + it 'returns false' do + expect(user.can_trigger_notifications?).to be(false) + end + end + + context 'when user is blocked' do + let_it_be(:user) { create(:user, :blocked) } + + it 'returns false' do + expect(user.can_trigger_notifications?).to be(false) + end + end + + context 'when user is a ghost' do + let_it_be(:user) { create(:user, :ghost) } + + it 'returns false' do + expect(user.can_trigger_notifications?).to be(false) + end + end + + context 'when user is confirmed and neither blocked or a ghost' do + let_it_be(:user) { create(:user) } + + it 'returns true' do + expect(user.can_trigger_notifications?).to be(true) + end + end + end + context 'bot users' do shared_examples 'bot users' do |bot_type| it 'creates the user if it does not exist' do @@ -5412,4 +5501,89 @@ RSpec.describe User do it_behaves_like 'bot user avatars', :support_bot, 'support-bot.png' it_behaves_like 'bot user avatars', :security_bot, 'security-bot.png' end + + describe '#confirmation_required_on_sign_in?' do + subject { user.confirmation_required_on_sign_in? } + + context 'when user is confirmed' do + let(:user) { build_stubbed(:user) } + + it 'is falsey' do + expect(user.confirmed?).to be_truthy + expect(subject).to be_falsey + end + end + + context 'when user is not confirmed' do + let_it_be(:user) { build_stubbed(:user, :unconfirmed, confirmation_sent_at: Time.current) } + + it 'is truthy when soft_email_confirmation feature is disabled' do + stub_feature_flags(soft_email_confirmation: false) + expect(subject).to be_truthy + end + + context 'when soft_email_confirmation feature is enabled' do + before do + stub_feature_flags(soft_email_confirmation: true) + end + + it 'is falsey when confirmation period is valid' do + expect(subject).to be_falsey + end + + it 'is truthy when confirmation period is expired' do + travel_to(User.allow_unconfirmed_access_for.from_now + 1.day) do + expect(subject).to be_truthy + end + end + + context 'when user has no confirmation email sent' do + let(:user) { build(:user, :unconfirmed, confirmation_sent_at: nil) } + + it 'is truthy' do + expect(subject).to be_truthy + end + end + end + end + end + + describe '#find_or_initialize_callout' do + subject(:find_or_initialize_callout) { user.find_or_initialize_callout(feature_name) } + + let(:user) { create(:user) } + let(:feature_name) { UserCallout.feature_names.each_key.first } + + context 'when callout exists' do + let!(:callout) { create(:user_callout, user: user, feature_name: feature_name) } + + it 'returns existing callout' do + expect(find_or_initialize_callout).to eq(callout) + end + end + + context 'when callout does not exist' do + context 'when feature name is valid' do + it 'initializes a new callout' do + expect(find_or_initialize_callout).to be_a_new(UserCallout) + end + + it 'is valid' do + expect(find_or_initialize_callout).to be_valid + end + end + + context 'when feature name is not valid' do + let(:feature_name) { 'notvalid' } + + it 'initializes a new callout' do + expect(find_or_initialize_callout).to be_a_new(UserCallout) + end + + it 'is not valid' do + expect(find_or_initialize_callout).not_to be_valid + end + end + end + end end |