diff options
Diffstat (limited to 'spec/models')
57 files changed, 1423 insertions, 1052 deletions
diff --git a/spec/models/alert_management/http_integration_spec.rb b/spec/models/alert_management/http_integration_spec.rb index 910df51801a..ddd65e723eb 100644 --- a/spec/models/alert_management/http_integration_spec.rb +++ b/spec/models/alert_management/http_integration_spec.rb @@ -38,7 +38,7 @@ RSpec.describe AlertManagement::HttpIntegration do context 'with valid JSON schema' do let(:attribute_mapping) do { - title: { path: %w(a b c), type: 'string' }, + title: { path: %w(a b c), type: 'string', label: 'Title' }, description: { path: %w(a), type: 'string' } } end diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index ea03cbc3706..4755d700d72 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -72,6 +72,7 @@ RSpec.describe ApplicationSetting do it { is_expected.not_to allow_value(nil).for(:push_event_activities_limit) } it { is_expected.to validate_numericality_of(:container_registry_delete_tags_service_timeout).only_integer.is_greater_than_or_equal_to(0) } + it { is_expected.to validate_numericality_of(:container_registry_cleanup_tags_service_max_list_size).only_integer.is_greater_than_or_equal_to(0) } it { is_expected.to validate_numericality_of(:container_registry_expiration_policies_worker_capacity).only_integer.is_greater_than_or_equal_to(0) } it { is_expected.to validate_numericality_of(:snippet_size_limit).only_integer.is_greater_than(0) } @@ -313,7 +314,7 @@ RSpec.describe ApplicationSetting do it { is_expected.to validate_presence_of(:max_attachment_size) } - it do + specify do is_expected.to validate_numericality_of(:max_attachment_size) .only_integer .is_greater_than(0) @@ -321,13 +322,13 @@ RSpec.describe ApplicationSetting do it { is_expected.to validate_presence_of(:max_import_size) } - it do + specify do is_expected.to validate_numericality_of(:max_import_size) .only_integer .is_greater_than_or_equal_to(0) end - it do + specify do is_expected.to validate_numericality_of(:local_markdown_version) .only_integer .is_greater_than_or_equal_to(0) @@ -472,7 +473,7 @@ RSpec.describe ApplicationSetting do end [:gitaly_timeout_default, :gitaly_timeout_medium, :gitaly_timeout_fast].each do |timeout_name| - it do + specify do is_expected.to validate_presence_of(timeout_name) is_expected.to validate_numericality_of(timeout_name).only_integer .is_greater_than_or_equal_to(0) @@ -733,6 +734,27 @@ RSpec.describe ApplicationSetting do is_expected.to be_invalid end end + + context 'throttle_* settings' do + where(:throttle_setting) do + %i[ + throttle_unauthenticated_requests_per_period + throttle_unauthenticated_period_in_seconds + throttle_authenticated_api_requests_per_period + throttle_authenticated_api_period_in_seconds + throttle_authenticated_web_requests_per_period + throttle_authenticated_web_period_in_seconds + ] + end + + with_them do + it { is_expected.to allow_value(3).for(throttle_setting) } + it { is_expected.not_to allow_value(-3).for(throttle_setting) } + it { is_expected.not_to allow_value(0).for(throttle_setting) } + it { is_expected.not_to allow_value('three').for(throttle_setting) } + it { is_expected.not_to allow_value(nil).for(throttle_setting) } + end + end end context 'restrict creating duplicates' do @@ -821,7 +843,7 @@ RSpec.describe ApplicationSetting do context 'validations' do it { is_expected.to validate_presence_of(:diff_max_patch_bytes) } - it do + specify do is_expected.to validate_numericality_of(:diff_max_patch_bytes) .only_integer .is_greater_than_or_equal_to(Gitlab::Git::Diff::DEFAULT_MAX_PATCH_BYTES) @@ -850,12 +872,13 @@ RSpec.describe ApplicationSetting do end end - describe '#instance_review_permitted?', :request_store do + describe '#instance_review_permitted?', :request_store, :use_clean_rails_memory_store_caching do subject { setting.instance_review_permitted? } before do - RequestStore.store[:current_license] = nil - expect(Rails.cache).to receive(:fetch).and_return( + allow(License).to receive(:current).and_return(nil) if Gitlab.ee? + allow(Rails.cache).to receive(:fetch).and_call_original + expect(Rails.cache).to receive(:fetch).with('limited_users_count', anything).and_return( ::ApplicationSetting::INSTANCE_REVIEW_MIN_USERS + users_over_minimum ) end diff --git a/spec/models/audit_event_partitioned_spec.rb b/spec/models/audit_event_archived_spec.rb index ab48e291f78..43a2e8434b0 100644 --- a/spec/models/audit_event_partitioned_spec.rb +++ b/spec/models/audit_event_archived_spec.rb @@ -2,35 +2,35 @@ require 'spec_helper' -RSpec.describe AuditEventPartitioned do +RSpec.describe AuditEventArchived do let(:source_table) { AuditEvent } - let(:partitioned_table) { described_class } + let(:destination_table) { described_class } it 'has the same columns as the source table' do column_names_from_source_table = column_names(source_table) - column_names_from_partioned_table = column_names(partitioned_table) + column_names_from_destination_table = column_names(destination_table) - expect(column_names_from_partioned_table).to match_array(column_names_from_source_table) + expect(column_names_from_destination_table).to match_array(column_names_from_source_table) end it 'has the same null constraints as the source table' do constraints_from_source_table = null_constraints(source_table) - constraints_from_partitioned_table = null_constraints(partitioned_table) + constraints_from_destination_table = null_constraints(destination_table) - expect(constraints_from_partitioned_table.to_a).to match_array(constraints_from_source_table.to_a) + expect(constraints_from_destination_table.to_a).to match_array(constraints_from_source_table.to_a) end it 'inserts the same record as the one in the source table', :aggregate_failures do - expect { create(:audit_event) }.to change { partitioned_table.count }.by(1) + expect { create(:audit_event) }.to change { destination_table.count }.by(1) event_from_source_table = source_table.connection.select_one( "SELECT * FROM #{source_table.table_name} ORDER BY created_at desc LIMIT 1" ) - event_from_partitioned_table = partitioned_table.connection.select_one( - "SELECT * FROM #{partitioned_table.table_name} ORDER BY created_at desc LIMIT 1" + event_from_destination_table = destination_table.connection.select_one( + "SELECT * FROM #{destination_table.table_name} ORDER BY created_at desc LIMIT 1" ) - expect(event_from_partitioned_table).to eq(event_from_source_table) + expect(event_from_destination_table).to eq(event_from_source_table) end def column_names(table) diff --git a/spec/models/ci/bridge_spec.rb b/spec/models/ci/bridge_spec.rb index 11dcecd50ca..4f09f6f1da4 100644 --- a/spec/models/ci/bridge_spec.rb +++ b/spec/models/ci/bridge_spec.rb @@ -356,14 +356,6 @@ RSpec.describe Ci::Bridge do describe '#dependency_variables' do subject { bridge.dependency_variables } - shared_context 'when ci_bridge_dependency_variables is disabled' do - before do - stub_feature_flags(ci_bridge_dependency_variables: false) - end - - it { is_expected.to be_empty } - end - context 'when downloading from previous stages' do let!(:prepare1) { create(:ci_build, name: 'prepare1', pipeline: pipeline, stage_idx: 0) } let!(:bridge) { create(:ci_bridge, pipeline: pipeline, stage_idx: 1) } @@ -374,8 +366,6 @@ RSpec.describe Ci::Bridge do it 'inherits only dependent variables' do expect(subject.to_hash).to eq(job_variable_1.key => job_variable_1.value) end - - it_behaves_like 'when ci_bridge_dependency_variables is disabled' end context 'when using needs' do @@ -397,8 +387,6 @@ RSpec.describe Ci::Bridge do it 'inherits only needs with artifacts variables' do expect(subject.to_hash).to eq(job_variable_1.key => job_variable_1.value) end - - it_behaves_like 'when ci_bridge_dependency_variables is disabled' end end end diff --git a/spec/models/ci/build_need_spec.rb b/spec/models/ci/build_need_spec.rb index 43cce073918..c2cf9027055 100644 --- a/spec/models/ci/build_need_spec.rb +++ b/spec/models/ci/build_need_spec.rb @@ -5,7 +5,7 @@ require 'spec_helper' RSpec.describe Ci::BuildNeed, model: true do let(:build_need) { build(:ci_build_need) } - it { is_expected.to belong_to(:build) } + it { is_expected.to belong_to(:build).class_name('Ci::Processable') } it { is_expected.to validate_presence_of(:build) } it { is_expected.to validate_presence_of(:name) } diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 9f412d64d56..c2029b9240b 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -717,6 +717,22 @@ RSpec.describe Ci::Build do end end + describe '#artifacts_public?' do + subject { build.artifacts_public? } + + context 'artifacts with defaults' do + let(:build) { create(:ci_build, :artifacts) } + + it { is_expected.to be_truthy } + end + + context 'non public artifacts' do + let(:build) { create(:ci_build, :artifacts, :non_public_artifacts) } + + it { is_expected.to be_falsey } + end + end + describe '#artifacts_expired?' do subject { build.artifacts_expired? } @@ -1149,26 +1165,12 @@ RSpec.describe Ci::Build do end context 'when transits to skipped' do - context 'when cd_skipped_deployment_status is disabled' do - before do - stub_feature_flags(cd_skipped_deployment_status: false) - build.skip! - end - - it 'transits deployment status to canceled' do - expect(deployment).to be_canceled - end + before do + build.skip! end - context 'when cd_skipped_deployment_status is enabled' do - before do - stub_feature_flags(cd_skipped_deployment_status: project) - build.skip! - end - - it 'transits deployment status to skipped' do - expect(deployment).to be_skipped - end + it 'transits deployment status to skipped' do + expect(deployment).to be_skipped end end @@ -2456,6 +2458,7 @@ RSpec.describe Ci::Build do { key: 'CI_PROJECT_VISIBILITY', value: 'private', public: true, masked: false }, { 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_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 }, @@ -2986,7 +2989,7 @@ RSpec.describe Ci::Build do let(:ci_config_path) { { key: 'CI_CONFIG_PATH', value: 'custom', public: true, masked: false } } before do - expect_any_instance_of(Project).to receive(:ci_config_path) { 'custom' } + project.update!(ci_config_path: 'custom') end it { is_expected.to include(ci_config_path) } @@ -4343,7 +4346,7 @@ RSpec.describe Ci::Build do end describe '#supported_runner?' do - let_it_be(:build) { create(:ci_build) } + let_it_be_with_refind(:build) { create(:ci_build) } subject { build.supported_runner?(runner_features) } @@ -4408,6 +4411,41 @@ RSpec.describe Ci::Build do it { is_expected.to be_falsey } end end + + context 'when `return_exit_code` feature is required by build' do + let(:options) { { allow_failure_criteria: { exit_codes: [1] } } } + + before do + build.update!(options: options) + end + + context 'when runner provides given feature' do + let(:runner_features) { { return_exit_code: true } } + + it { is_expected.to be_truthy } + end + + context 'when runner does not provide given feature' do + let(:runner_features) { {} } + + it { is_expected.to be_falsey } + end + + context 'when the runner does not provide all of the required features' do + let(:options) do + { + allow_failure_criteria: { exit_codes: [1] }, + artifacts: { reports: { junit: "junit.xml" } } + } + end + + let(:runner_features) { { return_exit_code: true } } + + it 'requires `upload_multiple_artifacts` too' do + is_expected.to be_falsey + end + end + end end describe '#deployment_status' do @@ -4737,22 +4775,6 @@ RSpec.describe Ci::Build do describe '#debug_mode?' do subject { build.debug_mode? } - context 'when feature is disabled' do - before do - stub_feature_flags(restrict_access_to_build_debug_mode: false) - end - - it { is_expected.to eq false } - - context 'when in variables' do - before do - create(:ci_instance_variable, key: 'CI_DEBUG_TRACE', value: 'true') - end - - it { is_expected.to eq false } - end - end - context 'when CI_DEBUG_TRACE=true is in variables' do context 'when in instance variables' do before do @@ -4807,4 +4829,159 @@ RSpec.describe Ci::Build do it { is_expected.to eq false } end end + + describe '#drop_with_exit_code!' do + let(:exit_code) { 1 } + let(:options) { {} } + + before do + build.options.merge!(options) + build.save! + end + + subject(:drop_with_exit_code) do + build.drop_with_exit_code!(:unknown_failure, exit_code) + end + + shared_examples 'drops the build without changing allow_failure' do + it 'does not change allow_failure' do + expect { drop_with_exit_code } + .not_to change { build.reload.allow_failure } + end + + it 'drops the build' do + expect { drop_with_exit_code } + .to change { build.reload.failed? } + end + end + + context 'when exit_codes are not defined' do + it_behaves_like 'drops the build without changing allow_failure' + end + + context 'when allow_failure_criteria is nil' do + let(:options) { { allow_failure_criteria: nil } } + + it_behaves_like 'drops the build without changing allow_failure' + end + + context 'when exit_codes is nil' do + let(:options) do + { + allow_failure_criteria: { + exit_codes: nil + } + } + end + + it_behaves_like 'drops the build without changing allow_failure' + end + + context 'when exit_codes do not match' do + let(:options) do + { + allow_failure_criteria: { + exit_codes: [2, 3, 4] + } + } + end + + it_behaves_like 'drops the build without changing allow_failure' + end + + context 'with matching exit codes' do + let(:options) do + { allow_failure_criteria: { exit_codes: [1, 2, 3] } } + end + + it 'changes allow_failure' do + expect { drop_with_exit_code } + .to change { build.reload.allow_failure } + end + + it 'drops the build' do + expect { drop_with_exit_code } + .to change { build.reload.failed? } + end + + it 'is executed inside a transaction' do + expect(build).to receive(:drop!) + .with(:unknown_failure) + .and_raise(ActiveRecord::Rollback) + + expect(build).to receive(:conditionally_allow_failure!) + .with(1) + .and_call_original + + expect { drop_with_exit_code } + .not_to change { build.reload.allow_failure } + end + + context 'when exit_code is nil' do + let(:exit_code) {} + + it_behaves_like 'drops the build without changing allow_failure' + end + + context 'when ci_allow_failure_with_exit_codes is disabled' do + before do + stub_feature_flags(ci_allow_failure_with_exit_codes: false) + end + + it_behaves_like 'drops the build without changing allow_failure' + end + end + end + + describe '#exit_codes_defined?' do + let(:options) { {} } + + before do + build.options.merge!(options) + end + + subject(:exit_codes_defined) do + build.exit_codes_defined? + end + + context 'without allow_failure_criteria' do + it { is_expected.to be_falsey } + end + + context 'when exit_codes is nil' do + let(:options) do + { + allow_failure_criteria: { + exit_codes: nil + } + } + end + + it { is_expected.to be_falsey } + end + + context 'when exit_codes is an empty array' do + let(:options) do + { + allow_failure_criteria: { + exit_codes: [] + } + } + end + + it { is_expected.to be_falsey } + end + + context 'when exit_codes are defined' do + let(:options) do + { + allow_failure_criteria: { + exit_codes: [5, 6] + } + } + end + + it { is_expected.to be_truthy } + end + end end diff --git a/spec/models/commit_with_pipeline_spec.rb b/spec/models/ci/commit_with_pipeline_spec.rb index c4b6deebae0..4dd288bde62 100644 --- a/spec/models/commit_with_pipeline_spec.rb +++ b/spec/models/ci/commit_with_pipeline_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe CommitWithPipeline do +RSpec.describe Ci::CommitWithPipeline do let(:project) { create(:project, :public, :repository) } let(:commit) { described_class.new(project.commit) } diff --git a/spec/models/ci/group_spec.rb b/spec/models/ci/group_spec.rb index c20b7e61044..6c96e659a34 100644 --- a/spec/models/ci/group_spec.rb +++ b/spec/models/ci/group_spec.rb @@ -54,6 +54,18 @@ RSpec.describe Ci::Group do .to be_a(Gitlab::Ci::Status::Failed) end end + + context 'when one of the commit statuses in the group is allowed to fail' do + let(:jobs) do + [create(:ci_build, :failed, :allowed_to_fail), + create(:ci_build, :success)] + end + + it 'fabricates a new detailed status object' do + expect(subject.detailed_status(double(:user))) + .to be_a(Gitlab::Ci::Status::SuccessWarning) + end + end end describe '.fabricate' do diff --git a/spec/models/ci/job_artifact_spec.rb b/spec/models/ci/job_artifact_spec.rb index ef21ca8f100..796947be4c8 100644 --- a/spec/models/ci/job_artifact_spec.rb +++ b/spec/models/ci/job_artifact_spec.rb @@ -219,6 +219,39 @@ RSpec.describe Ci::JobArtifact do end end + describe '.unlocked' do + let_it_be(:job_artifact) { create(:ci_job_artifact) } + + context 'with locked pipelines' do + before do + job_artifact.job.pipeline.artifacts_locked! + end + + it 'returns an empty array' do + expect(described_class.unlocked).to be_empty + end + end + + context 'with unlocked pipelines' do + before do + job_artifact.job.pipeline.unlocked! + end + + it 'returns the artifact' do + expect(described_class.unlocked).to eq([job_artifact]) + end + end + end + + describe '.order_expired_desc' do + let_it_be(:first_artifact) { create(:ci_job_artifact, expire_at: 2.days.ago) } + let_it_be(:second_artifact) { create(:ci_job_artifact, expire_at: 1.day.ago) } + + it 'returns ordered artifacts' do + expect(described_class.order_expired_desc).to eq([second_artifact, first_artifact]) + end + end + describe 'callbacks' do describe '#schedule_background_upload' do subject { create(:ci_job_artifact, :archive) } diff --git a/spec/models/ci/ref_spec.rb b/spec/models/ci/ref_spec.rb index cb62646532c..0a9cd5ef2ec 100644 --- a/spec/models/ci/ref_spec.rb +++ b/spec/models/ci/ref_spec.rb @@ -16,35 +16,49 @@ RSpec.describe Ci::Ref do stub_const('Ci::PipelineSuccessUnlockArtifactsWorker', unlock_artifacts_worker_spy) end - where(:initial_state, :action, :count) do - :unknown | :succeed! | 1 - :unknown | :do_fail! | 0 - :success | :succeed! | 1 - :success | :do_fail! | 0 - :failed | :succeed! | 1 - :failed | :do_fail! | 0 - :fixed | :succeed! | 1 - :fixed | :do_fail! | 0 - :broken | :succeed! | 1 - :broken | :do_fail! | 0 - :still_failing | :succeed | 1 - :still_failing | :do_fail | 0 - end + context 'pipline is locked' do + let!(:pipeline) { create(:ci_pipeline, ci_ref_id: ci_ref.id, locked: :artifacts_locked) } + + where(:initial_state, :action, :count) do + :unknown | :succeed! | 1 + :unknown | :do_fail! | 0 + :success | :succeed! | 1 + :success | :do_fail! | 0 + :failed | :succeed! | 1 + :failed | :do_fail! | 0 + :fixed | :succeed! | 1 + :fixed | :do_fail! | 0 + :broken | :succeed! | 1 + :broken | :do_fail! | 0 + :still_failing | :succeed | 1 + :still_failing | :do_fail | 0 + end - with_them do - context "when transitioning states" do - before do - status_value = Ci::Ref.state_machines[:status].states[initial_state].value - ci_ref.update!(status: status_value) - end + with_them do + context "when transitioning states" do + before do + status_value = Ci::Ref.state_machines[:status].states[initial_state].value + ci_ref.update!(status: status_value) + end - it 'calls unlock artifacts service' do - ci_ref.send(action) + it 'calls unlock artifacts service' do + ci_ref.send(action) - expect(unlock_artifacts_worker_spy).to have_received(:perform_async).exactly(count).times + expect(unlock_artifacts_worker_spy).to have_received(:perform_async).exactly(count).times + end end end end + + context 'pipeline is unlocked' do + let!(:pipeline) { create(:ci_pipeline, ci_ref_id: ci_ref.id, locked: :unlocked) } + + it 'does not call unlock artifacts service' do + ci_ref.succeed! + + expect(unlock_artifacts_worker_spy).not_to have_received(:perform_async) + end + end end end diff --git a/spec/models/clusters/applications/knative_spec.rb b/spec/models/clusters/applications/knative_spec.rb index 41b4ec86233..d0e470bfa42 100644 --- a/spec/models/clusters/applications/knative_spec.rb +++ b/spec/models/clusters/applications/knative_spec.rb @@ -150,7 +150,7 @@ RSpec.describe Clusters::Applications::Knative do subject { knative.install_command } it 'is initialized with latest version' do - expect(subject.version).to eq('0.9.0') + expect(subject.version).to eq('0.10.0') end it_behaves_like 'a command' @@ -204,8 +204,8 @@ RSpec.describe Clusters::Applications::Knative do expect(subject.postdelete).to include(*remove_knative_istio_leftovers_script) expect(subject.postdelete.size).to eq(full_delete_commands_size) - expect(subject.postdelete[2]).to eq("kubectl api-resources -o name --api-group #{api_groups[0]} | xargs kubectl delete --ignore-not-found crd") - expect(subject.postdelete[3]).to eq("kubectl api-resources -o name --api-group #{api_groups[1]} | xargs kubectl delete --ignore-not-found crd") + expect(subject.postdelete[2]).to include("kubectl api-resources -o name --api-group #{api_groups[0]} | xargs -r kubectl delete --ignore-not-found crd") + expect(subject.postdelete[3]).to include("kubectl api-resources -o name --api-group #{api_groups[1]} | xargs -r kubectl delete --ignore-not-found crd") end end diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb index cfa87b3e39e..acbabee9383 100644 --- a/spec/models/commit_spec.rb +++ b/spec/models/commit_spec.rb @@ -428,6 +428,19 @@ eos allow(commit).to receive(:safe_message).and_return(message) expect(commit.description).to eq(message) end + + it 'truncates html representation if more than 1Mib' do + # Commit message is over 2MiB + huge_commit_message = ['panic', ('panic ' * 350000), 'trailing text'].join("\n") + + allow(commit).to receive(:safe_message).and_return(huge_commit_message) + + commit.refresh_markdown_cache + description_html = commit.description_html + + expect(description_html.bytesize).to be < 2.megabytes + expect(description_html).not_to include('trailing text') + end end describe "delegation" do diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb index 9824eb91bc7..532f68c2f18 100644 --- a/spec/models/commit_status_spec.rb +++ b/spec/models/commit_status_spec.rb @@ -61,6 +61,22 @@ RSpec.describe CommitStatus do expect(commit_status.started_at).to be_present end end + + describe 'transitioning to created from skipped or manual' do + let(:commit_status) { create(:commit_status, :skipped) } + + it 'does not update user without parameter' do + commit_status.process! + + expect { commit_status.process }.not_to change { commit_status.reload.user } + end + + it 'updates user with user parameter' do + new_user = create(:user) + + expect { commit_status.process(new_user) }.to change { commit_status.reload.user }.to(new_user) + end + end end describe '#processed' do @@ -503,6 +519,9 @@ RSpec.describe CommitStatus do subject { commit_status.group_name } where(:name, :group_name) do + 'rspec1' | 'rspec1' + 'rspec1 0 1' | 'rspec1' + 'rspec1 0/2' | 'rspec1' 'rspec:windows' | 'rspec:windows' 'rspec:windows 0' | 'rspec:windows 0' 'rspec:windows 0 test' | 'rspec:windows 0 test' diff --git a/spec/models/concerns/ci/artifactable_spec.rb b/spec/models/concerns/ci/artifactable_spec.rb index f05189abdd2..ebc838e86a6 100644 --- a/spec/models/concerns/ci/artifactable_spec.rb +++ b/spec/models/concerns/ci/artifactable_spec.rb @@ -54,4 +54,23 @@ RSpec.describe Ci::Artifactable do end end end + + context 'ActiveRecord scopes' do + let_it_be(:recently_expired_artifact) { create(:ci_job_artifact, expire_at: 1.day.ago) } + let_it_be(:later_expired_artifact) { create(:ci_job_artifact, expire_at: 2.days.ago) } + let_it_be(:not_expired_artifact) { create(:ci_job_artifact, expire_at: 1.day.from_now) } + + describe '.expired_before' do + it 'returns expired artifacts' do + expect(Ci::JobArtifact.expired_before(1.hour.ago)) + .to match_array([recently_expired_artifact, later_expired_artifact]) + end + end + + describe '.expired' do + it 'returns a limited number of expired artifacts' do + expect(Ci::JobArtifact.expired(1).order_id_asc).to eq([recently_expired_artifact]) + end + end + end end diff --git a/spec/models/concerns/each_batch_spec.rb b/spec/models/concerns/each_batch_spec.rb index 8b70753633c..5f4e5d4bd98 100644 --- a/spec/models/concerns/each_batch_spec.rb +++ b/spec/models/concerns/each_batch_spec.rb @@ -56,5 +56,21 @@ RSpec.describe EachBatch do it_behaves_like 'each_batch handling', {} it_behaves_like 'each_batch handling', { order_hint: :updated_at } + + it 'orders ascending by default' do + ids = [] + + model.each_batch(of: 1) { |rel| ids.concat(rel.ids) } + + expect(ids).to eq(ids.sort) + end + + it 'accepts descending order' do + ids = [] + + model.each_batch(of: 1, order: :desc) { |rel| ids.concat(rel.ids) } + + expect(ids).to eq(ids.sort.reverse) + end end end diff --git a/spec/models/concerns/milestoneable_spec.rb b/spec/models/concerns/milestoneable_spec.rb index c37582cb65d..5fb3b39f734 100644 --- a/spec/models/concerns/milestoneable_spec.rb +++ b/spec/models/concerns/milestoneable_spec.rb @@ -104,8 +104,8 @@ RSpec.describe Milestoneable do context "for incidents" do let(:incident) { build(:incident) } - it 'returns false' do - expect(incident.supports_milestone?).to be_falsy + it 'returns true' do + expect(incident.supports_milestone?).to be_truthy end end end diff --git a/spec/models/concerns/noteable_spec.rb b/spec/models/concerns/noteable_spec.rb index bb7374bf46c..a7117af81a2 100644 --- a/spec/models/concerns/noteable_spec.rb +++ b/spec/models/concerns/noteable_spec.rb @@ -25,8 +25,8 @@ RSpec.describe Noteable do let(:active_position2) do Gitlab::Diff::Position.new( - old_path: "files/ruby/popen.rb", - new_path: "files/ruby/popen.rb", + old_path: 'files/ruby/popen.rb', + new_path: 'files/ruby/popen.rb', old_line: 16, new_line: 22, diff_refs: subject.diff_refs @@ -35,11 +35,11 @@ RSpec.describe Noteable do let(:outdated_position) do Gitlab::Diff::Position.new( - old_path: "files/ruby/popen.rb", - new_path: "files/ruby/popen.rb", + old_path: 'files/ruby/popen.rb', + new_path: 'files/ruby/popen.rb', old_line: nil, new_line: 9, - diff_refs: project.commit("874797c3a73b60d2187ed6e2fcabd289ff75171e").diff_refs + diff_refs: project.commit('874797c3a73b60d2187ed6e2fcabd289ff75171e').diff_refs ) end @@ -80,7 +80,7 @@ RSpec.describe Noteable do describe '#grouped_diff_discussions' do let(:grouped_diff_discussions) { subject.grouped_diff_discussions } - it "includes active discussions" do + it 'includes active discussions' do discussions = grouped_diff_discussions.values.flatten expect(discussions.count).to eq(2) @@ -91,17 +91,17 @@ RSpec.describe Noteable do expect(discussions.last.notes).to eq([active_diff_note3]) end - it "doesn't include outdated discussions" do + it 'does not include outdated discussions' do expect(grouped_diff_discussions.values.flatten.map(&:id)).not_to include(outdated_diff_note1.discussion_id) end - it "groups the discussions by line code" do + it 'groups the discussions by line code' do expect(grouped_diff_discussions[active_diff_note1.line_code].first.id).to eq(active_diff_note1.discussion_id) expect(grouped_diff_discussions[active_diff_note3.line_code].first.id).to eq(active_diff_note3.discussion_id) end end - context "discussion status" do + context 'discussion status' do let(:first_discussion) { build_stubbed(:discussion_note_on_merge_request, noteable: subject, project: project).to_discussion } let(:second_discussion) { build_stubbed(:discussion_note_on_merge_request, noteable: subject, project: project).to_discussion } let(:third_discussion) { build_stubbed(:discussion_note_on_merge_request, noteable: subject, project: project).to_discussion } @@ -110,56 +110,56 @@ RSpec.describe Noteable do allow(subject).to receive(:resolvable_discussions).and_return([first_discussion, second_discussion, third_discussion]) end - describe "#discussions_resolvable?" do - context "when all discussions are unresolvable" do + describe '#discussions_resolvable?' do + context 'when all discussions are unresolvable' do before do allow(first_discussion).to receive(:resolvable?).and_return(false) allow(second_discussion).to receive(:resolvable?).and_return(false) allow(third_discussion).to receive(:resolvable?).and_return(false) end - it "returns false" do + it 'returns false' do expect(subject.discussions_resolvable?).to be false end end - context "when some discussions are unresolvable and some discussions are resolvable" do + context 'when some discussions are unresolvable and some discussions are resolvable' do before do allow(first_discussion).to receive(:resolvable?).and_return(true) allow(second_discussion).to receive(:resolvable?).and_return(false) allow(third_discussion).to receive(:resolvable?).and_return(true) end - it "returns true" do + it 'returns true' do expect(subject.discussions_resolvable?).to be true end end - context "when all discussions are resolvable" do + context 'when all discussions are resolvable' do before do allow(first_discussion).to receive(:resolvable?).and_return(true) allow(second_discussion).to receive(:resolvable?).and_return(true) allow(third_discussion).to receive(:resolvable?).and_return(true) end - it "returns true" do + it 'returns true' do expect(subject.discussions_resolvable?).to be true end end end - describe "#discussions_resolved?" do - context "when discussions are not resolvable" do + describe '#discussions_resolved?' do + context 'when discussions are not resolvable' do before do allow(subject).to receive(:discussions_resolvable?).and_return(false) end - it "returns false" do + it 'returns false' do expect(subject.discussions_resolved?).to be false end end - context "when discussions are resolvable" do + context 'when discussions are resolvable' do before do allow(subject).to receive(:discussions_resolvable?).and_return(true) @@ -168,31 +168,31 @@ RSpec.describe Noteable do allow(third_discussion).to receive(:resolvable?).and_return(true) end - context "when all resolvable discussions are resolved" do + context 'when all resolvable discussions are resolved' do before do allow(first_discussion).to receive(:resolved?).and_return(true) allow(third_discussion).to receive(:resolved?).and_return(true) end - it "returns true" do + it 'returns true' do expect(subject.discussions_resolved?).to be true end end - context "when some resolvable discussions are not resolved" do + context 'when some resolvable discussions are not resolved' do before do allow(first_discussion).to receive(:resolved?).and_return(true) allow(third_discussion).to receive(:resolved?).and_return(false) end - it "returns false" do + it 'returns false' do expect(subject.discussions_resolved?).to be false end end end end - describe "#discussions_to_be_resolved" do + describe '#discussions_to_be_resolved' do before do allow(first_discussion).to receive(:to_be_resolved?).and_return(true) allow(second_discussion).to receive(:to_be_resolved?).and_return(false) @@ -245,6 +245,12 @@ RSpec.describe Noteable do end end + describe '.email_creatable_types' do + it 'exposes the email creatable types' do + expect(described_class.email_creatable_types).to include('Issue') + end + end + describe '#capped_notes_count' do context 'notes number < 10' do it 'the number of notes is returned' do @@ -263,13 +269,13 @@ RSpec.describe Noteable do end end - describe "#has_any_diff_note_positions?" do - let(:source_branch) { "compare-with-merge-head-source" } - let(:target_branch) { "compare-with-merge-head-target" } + describe '#has_any_diff_note_positions?' do + let(:source_branch) { 'compare-with-merge-head-source' } + let(:target_branch) { 'compare-with-merge-head-target' } let(:merge_request) { create(:merge_request, source_branch: source_branch, target_branch: target_branch) } let!(:note) do - path = "files/markdown/ruby-style-guide.md" + path = 'files/markdown/ruby-style-guide.md' position = Gitlab::Diff::Position.new( old_path: path, @@ -286,20 +292,54 @@ RSpec.describe Noteable do Discussions::CaptureDiffNotePositionsService.new(merge_request).execute end - it "returns true when it has diff note positions" do + it 'returns true when it has diff note positions' do expect(merge_request.has_any_diff_note_positions?).to be(true) end - it "returns false when it has notes but no diff note positions" do + it 'returns false when it has notes but no diff note positions' do DiffNotePosition.where(note: note).find_each(&:delete) expect(merge_request.has_any_diff_note_positions?).to be(false) end - it "returns false when it has no notes" do + it 'returns false when it has no notes' do merge_request.notes.find_each(&:destroy) expect(merge_request.has_any_diff_note_positions?).to be(false) end end + + describe '#creatable_note_email_address' do + let_it_be(:user) { create(:user) } + let_it_be(:source_branch) { 'compare-with-merge-head-source' } + + let(:issue) { create(:issue, project: project) } + let(:snippet) { build(:snippet) } + + context 'incoming email enabled' do + before do + stub_incoming_email_setting(enabled: true, address: "p+%{key}@gl.ab") + end + + it 'returns the address to create a note' do + address = "p+#{project.full_path_slug}-#{project.project_id}-#{user.incoming_email_token}-issue-#{issue.iid}@gl.ab" + + expect(issue.creatable_note_email_address(user)).to eq(address) + end + + it 'returns nil for unsupported types' do + expect(snippet.creatable_note_email_address(user)).to be_nil + end + end + + context 'incoming email disabled' do + before do + stub_incoming_email_setting(enabled: false) + end + + it 'returns nil' do + expect(issue.creatable_note_email_address(user)).to be_nil + end + end + end end diff --git a/spec/models/cycle_analytics/code_spec.rb b/spec/models/cycle_analytics/code_spec.rb deleted file mode 100644 index ca612cba654..00000000000 --- a/spec/models/cycle_analytics/code_spec.rb +++ /dev/null @@ -1,73 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe 'CycleAnalytics#code' do - extend CycleAnalyticsHelpers::TestGeneration - - let_it_be(:project) { create(:project, :repository) } - let_it_be(:from_date) { 10.days.ago } - let_it_be(:user) { project.owner } - let_it_be(:project_level) { CycleAnalytics::ProjectLevel.new(project, options: { from: from_date, current_user: user }) } - - subject { project_level } - - context 'with deployment' do - generate_cycle_analytics_spec( - phase: :code, - data_fn: -> (context) { { issue: context.create(:issue, project: context.project) } }, - start_time_conditions: [["issue mentioned in a commit", - -> (context, data) do - context.create_commit_referencing_issue(data[:issue]) - end]], - end_time_conditions: [["merge request that closes issue is created", - -> (context, data) do - context.create_merge_request_closing_issue(context.user, context.project, data[:issue]) - end]], - post_fn: -> (context, data) do - end) - - context "when a regular merge request (that doesn't close the issue) is created" do - it "returns nil" do - issue = create(:issue, project: project) - - create_commit_referencing_issue(issue) - create_merge_request_closing_issue(user, project, issue, message: "Closes nothing") - - merge_merge_requests_closing_issue(user, project, issue) - deploy_master(user, project) - - expect(subject[:code].project_median).to be_nil - end - end - end - - context 'without deployment' do - generate_cycle_analytics_spec( - phase: :code, - data_fn: -> (context) { { issue: context.create(:issue, project: context.project) } }, - start_time_conditions: [["issue mentioned in a commit", - -> (context, data) do - context.create_commit_referencing_issue(data[:issue]) - end]], - end_time_conditions: [["merge request that closes issue is created", - -> (context, data) do - context.create_merge_request_closing_issue(context.user, context.project, data[:issue]) - end]], - post_fn: -> (context, data) do - end) - - context "when a regular merge request (that doesn't close the issue) is created" do - it "returns nil" do - issue = create(:issue, project: project) - - create_commit_referencing_issue(issue) - create_merge_request_closing_issue(user, project, issue, message: "Closes nothing") - - merge_merge_requests_closing_issue(user, project, issue) - - expect(subject[:code].project_median).to be_nil - end - end - end -end diff --git a/spec/models/cycle_analytics/issue_spec.rb b/spec/models/cycle_analytics/issue_spec.rb deleted file mode 100644 index 66d21f6925f..00000000000 --- a/spec/models/cycle_analytics/issue_spec.rb +++ /dev/null @@ -1,46 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe 'CycleAnalytics#issue' do - extend CycleAnalyticsHelpers::TestGeneration - - let_it_be(:project) { create(:project, :repository) } - let_it_be(:from_date) { 10.days.ago } - let_it_be(:user) { project.owner } - let_it_be(:project_level) { CycleAnalytics::ProjectLevel.new(project, options: { from: from_date, current_user: user }) } - - subject { project_level } - - generate_cycle_analytics_spec( - phase: :issue, - data_fn: -> (context) { { issue: context.build(:issue, project: context.project) } }, - start_time_conditions: [["issue created", -> (context, data) { data[:issue].save! }]], - end_time_conditions: [["issue associated with a milestone", - -> (context, data) do - if data[:issue].persisted? - data[:issue].update!(milestone: context.create(:milestone, project: context.project)) - end - end], - ["list label added to issue", - -> (context, data) do - if data[:issue].persisted? - data[:issue].update!(label_ids: [context.create(:list).label_id]) - end - end]], - post_fn: -> (context, data) do - end) - - context "when a regular label (instead of a list label) is added to the issue" do - it "returns nil" do - regular_label = create(:label) - issue = create(:issue, project: project) - issue.update!(label_ids: [regular_label.id]) - - create_merge_request_closing_issue(user, project, issue) - merge_merge_requests_closing_issue(user, project, issue) - - expect(subject[:issue].project_median).to be_nil - end - end -end diff --git a/spec/models/cycle_analytics/plan_spec.rb b/spec/models/cycle_analytics/plan_spec.rb deleted file mode 100644 index acaf767db01..00000000000 --- a/spec/models/cycle_analytics/plan_spec.rb +++ /dev/null @@ -1,52 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe 'CycleAnalytics#plan' do - extend CycleAnalyticsHelpers::TestGeneration - - let_it_be(:project) { create(:project, :repository) } - let_it_be(:from_date) { 10.days.ago } - let_it_be(:user) { project.owner } - let_it_be(:project_level) { CycleAnalytics::ProjectLevel.new(project, options: { from: from_date, current_user: user }) } - - subject { project_level } - - generate_cycle_analytics_spec( - phase: :plan, - data_fn: -> (context) do - { - issue: context.build(:issue, project: context.project), - branch_name: context.generate(:branch) - } - end, - start_time_conditions: [["issue associated with a milestone", - -> (context, data) do - data[:issue].update!(milestone: context.create(:milestone, project: context.project)) - end], - ["list label added to issue", - -> (context, data) do - data[:issue].update!(label_ids: [context.create(:list).label_id]) - end]], - end_time_conditions: [["issue mentioned in a commit", - -> (context, data) do - context.create_commit_referencing_issue(data[:issue], branch_name: data[:branch_name]) - end]], - post_fn: -> (context, data) do - end) - - context "when a regular label (instead of a list label) is added to the issue" do - it "returns nil" do - branch_name = generate(:branch) - label = create(:label) - issue = create(:issue, project: project) - issue.update!(label_ids: [label.id]) - create_commit_referencing_issue(issue, branch_name: branch_name) - - create_merge_request_closing_issue(user, project, issue, source_branch: branch_name) - merge_merge_requests_closing_issue(user, project, issue) - - expect(subject[:issue].project_median).to be_nil - end - end -end diff --git a/spec/models/cycle_analytics/project_level_spec.rb b/spec/models/cycle_analytics/project_level_spec.rb deleted file mode 100644 index c2d421c03d8..00000000000 --- a/spec/models/cycle_analytics/project_level_spec.rb +++ /dev/null @@ -1,34 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe CycleAnalytics::ProjectLevel do - let_it_be(:project) { create(:project, :repository) } - let_it_be(:from_date) { 10.days.ago } - let_it_be(:user) { project.owner } - let_it_be(:issue) { create(:issue, project: project, created_at: 2.days.ago) } - let_it_be(:milestone) { create(:milestone, project: project) } - let(:mr) { create_merge_request_closing_issue(user, project, issue, commit_message: "References #{issue.to_reference}") } - let(:pipeline) { create(:ci_empty_pipeline, status: 'created', project: project, ref: mr.source_branch, sha: mr.source_branch_sha, head_pipeline_of: mr) } - - subject { described_class.new(project, options: { from: from_date }) } - - describe '#all_medians_by_stage' do - before do - allow_next_instance_of(Gitlab::ReferenceExtractor) do |instance| - allow(instance).to receive(:issues).and_return([issue]) - end - - create_cycle(user, project, issue, mr, milestone, pipeline) - deploy_master(user, project) - end - - it 'returns every median for each stage for a specific project' do - values = described_class::STAGES.each_with_object({}) do |stage_name, hsh| - hsh[stage_name] = subject[stage_name].project_median.presence - end - - expect(subject.all_medians_by_stage).to eq(values) - end - end -end diff --git a/spec/models/cycle_analytics/project_level_stage_adapter_spec.rb b/spec/models/cycle_analytics/project_level_stage_adapter_spec.rb new file mode 100644 index 00000000000..9bdee292938 --- /dev/null +++ b/spec/models/cycle_analytics/project_level_stage_adapter_spec.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe CycleAnalytics::ProjectLevelStageAdapter, type: :model do + let_it_be(:stage_name) { :review } # pre-defined, default stage + let_it_be(:merge_request) do + create(:merge_request, created_at: 5.hours.ago).tap do |mr| + mr.metrics.update!(merged_at: mr.created_at + 1.hour) + end + end + + let_it_be(:project) { merge_request.target_project } + + let(:stage) do + params = Gitlab::Analytics::CycleAnalytics::DefaultStages.find_by_name!(stage_name).merge(project: project) + Analytics::CycleAnalytics::ProjectStage.new(params) + end + + around do |example| + freeze_time { example.run } + end + + subject { described_class.new(stage, from: 1.month.ago, to: Time.zone.now, current_user: merge_request.author) } + + it 'calculates median' do + expect(subject.median).to be_within(1.hour).of(0.5) + end + + it 'lists events' do + expect(subject.events.size).to eq(1) + expect(subject.events.first[:title]).to eq(merge_request.title) + end + + it 'presents the data as json' do + expect(subject.as_json).to include({ title: 'Review', value: 'about 1 hour' }) + end +end diff --git a/spec/models/cycle_analytics/review_spec.rb b/spec/models/cycle_analytics/review_spec.rb deleted file mode 100644 index 06d9cfbf8c0..00000000000 --- a/spec/models/cycle_analytics/review_spec.rb +++ /dev/null @@ -1,34 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe 'CycleAnalytics#review' do - extend CycleAnalyticsHelpers::TestGeneration - - let_it_be(:project) { create(:project, :repository) } - let_it_be(:from_date) { 10.days.ago } - let_it_be(:user) { project.owner } - - subject { CycleAnalytics::ProjectLevel.new(project, options: { from: from_date, current_user: user }) } - - generate_cycle_analytics_spec( - phase: :review, - data_fn: -> (context) { { issue: context.create(:issue, project: context.project) } }, - start_time_conditions: [["merge request that closes issue is created", - -> (context, data) do - context.create_merge_request_closing_issue(context.user, context.project, data[:issue]) - end]], - end_time_conditions: [["merge request that closes issue is merged", - -> (context, data) do - context.merge_merge_requests_closing_issue(context.user, context.project, data[:issue]) - end]], - post_fn: nil) - - context "when a regular merge request (that doesn't close the issue) is created and merged" do - it "returns nil" do - MergeRequests::MergeService.new(project, user).execute(create(:merge_request)) - - expect(subject[:review].project_median).to be_nil - end - end -end diff --git a/spec/models/cycle_analytics/staging_spec.rb b/spec/models/cycle_analytics/staging_spec.rb deleted file mode 100644 index 50cb49d6309..00000000000 --- a/spec/models/cycle_analytics/staging_spec.rb +++ /dev/null @@ -1,56 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe 'CycleAnalytics#staging' do - extend CycleAnalyticsHelpers::TestGeneration - - let_it_be(:project) { create(:project, :repository) } - let_it_be(:from_date) { 10.days.ago } - let_it_be(:user) { project.owner } - let_it_be(:project_level) { CycleAnalytics::ProjectLevel.new(project, options: { from: from_date, current_user: user }) } - - subject { project_level } - - generate_cycle_analytics_spec( - phase: :staging, - data_fn: lambda do |context| - issue = context.create(:issue, project: context.project) - { issue: issue, merge_request: context.create_merge_request_closing_issue(context.user, context.project, issue) } - end, - start_time_conditions: [["merge request that closes issue is merged", - -> (context, data) do - context.merge_merge_requests_closing_issue(context.user, context.project, data[:issue]) - end]], - end_time_conditions: [["merge request that closes issue is deployed to production", - -> (context, data) do - context.deploy_master(context.user, context.project) - end], - ["production deploy happens after merge request is merged (along with other changes)", - lambda do |context, data| - # Make other changes on master - context.project.repository.commit("this_sha_apparently_does_not_matter") - context.deploy_master(context.user, context.project) - end]]) - - context "when a regular merge request (that doesn't close the issue) is merged and deployed" do - it "returns nil" do - merge_request = create(:merge_request) - MergeRequests::MergeService.new(project, user).execute(merge_request) - deploy_master(user, project) - - expect(subject[:staging].project_median).to be_nil - end - end - - context "when the deployment happens to a non-production environment" do - it "returns nil" do - issue = create(:issue, project: project) - merge_request = create_merge_request_closing_issue(user, project, issue) - MergeRequests::MergeService.new(project, user).execute(merge_request) - deploy_master(user, project, environment: 'staging') - - expect(subject[:staging].project_median).to be_nil - end - end -end diff --git a/spec/models/cycle_analytics/test_spec.rb b/spec/models/cycle_analytics/test_spec.rb deleted file mode 100644 index 8f65c047b15..00000000000 --- a/spec/models/cycle_analytics/test_spec.rb +++ /dev/null @@ -1,73 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe 'CycleAnalytics#test' do - extend CycleAnalyticsHelpers::TestGeneration - - let_it_be(:project) { create(:project, :repository) } - let_it_be(:from_date) { 10.days.ago } - let_it_be(:user) { project.owner } - let_it_be(:issue) { create(:issue, project: project) } - let_it_be(:project_level) { CycleAnalytics::ProjectLevel.new(project, options: { from: from_date, current_user: user }) } - let!(:merge_request) { create_merge_request_closing_issue(user, project, issue) } - - subject { project_level } - - generate_cycle_analytics_spec( - phase: :test, - data_fn: lambda do |context| - issue = context.issue - merge_request = context.create_merge_request_closing_issue(context.user, context.project, issue) - pipeline = context.create(:ci_pipeline, ref: merge_request.source_branch, sha: merge_request.diff_head_sha, project: context.project, head_pipeline_of: merge_request) - { pipeline: pipeline, issue: issue } - end, - start_time_conditions: [["pipeline is started", -> (context, data) { data[:pipeline].run! }]], - end_time_conditions: [["pipeline is finished", -> (context, data) { data[:pipeline].succeed! }]], - post_fn: -> (context, data) do - end) - - context "when the pipeline is for a regular merge request (that doesn't close an issue)" do - it "returns nil" do - pipeline = create(:ci_pipeline, ref: "refs/heads/#{merge_request.source_branch}", sha: merge_request.diff_head_sha) - - pipeline.run! - pipeline.succeed! - - expect(subject[:test].project_median).to be_nil - end - end - - context "when the pipeline is not for a merge request" do - it "returns nil" do - pipeline = create(:ci_pipeline, ref: "refs/heads/master", sha: project.repository.commit('master').sha) - - pipeline.run! - pipeline.succeed! - - expect(subject[:test].project_median).to be_nil - end - end - - context "when the pipeline is dropped (failed)" do - it "returns nil" do - pipeline = create(:ci_pipeline, ref: "refs/heads/#{merge_request.source_branch}", sha: merge_request.diff_head_sha) - - pipeline.run! - pipeline.drop! - - expect(subject[:test].project_median).to be_nil - end - end - - context "when the pipeline is cancelled" do - it "returns nil" do - pipeline = create(:ci_pipeline, ref: "refs/heads/#{merge_request.source_branch}", sha: merge_request.diff_head_sha) - - pipeline.run! - pipeline.cancel! - - expect(subject[:test].project_median).to be_nil - end - end -end diff --git a/spec/models/deployment_spec.rb b/spec/models/deployment_spec.rb index c962b012a4b..5bc61db6d21 100644 --- a/spec/models/deployment_spec.rb +++ b/spec/models/deployment_spec.rb @@ -227,6 +227,56 @@ RSpec.describe Deployment do deployment.skip! end end + + describe 'synching status to Jira' do + let(:deployment) { create(:deployment) } + + let(:worker) { ::JiraConnect::SyncDeploymentsWorker } + + it 'calls the worker on creation' do + expect(worker).to receive(:perform_async).with(Integer) + + deployment + end + + it 'does not call the worker for skipped deployments' do + expect(deployment).to be_present # warm-up, ignore the creation trigger + + expect(worker).not_to receive(:perform_async) + + deployment.skip! + end + + %i[run! succeed! drop! cancel!].each do |event| + context "when we call pipeline.#{event}" do + it 'triggers a Jira synch worker' do + expect(worker).to receive(:perform_async).with(deployment.id) + + deployment.send(event) + end + + context 'the feature is disabled' do + it 'does not trigger a worker' do + stub_feature_flags(jira_sync_deployments: false) + + expect(worker).not_to receive(:perform_async) + + deployment.send(event) + end + end + + context 'the feature is enabled for this project' do + it 'does trigger a worker' do + stub_feature_flags(jira_sync_deployments: deployment.project) + + expect(worker).to receive(:perform_async) + + deployment.send(event) + end + end + end + end + end end describe '#success?' do diff --git a/spec/models/experiment_spec.rb b/spec/models/experiment_spec.rb index 1bf7b8b4850..171bfd116d3 100644 --- a/spec/models/experiment_spec.rb +++ b/spec/models/experiment_spec.rb @@ -164,7 +164,7 @@ RSpec.describe Experiment do context 'when an experiment_user already exists for the given user' do before do # Create an existing experiment_user for this experiment and the :control group - experiment.record_user_and_group(user, :control, context) + experiment.record_user_and_group(user, :control) end it 'does not create a new experiment_user record' do @@ -173,15 +173,65 @@ RSpec.describe Experiment do context 'but the group_type and context has changed' do let(:group) { :experimental } - let(:context) { { b: 37 } } it 'updates the existing experiment_user record with group_type' do expect { record_user_and_group }.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] } } - it 'updates the existing experiment_user record with context' do + 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 - expect(ExperimentUser.last.context).to eq({ 'b' => 37 }) + 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 end diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index cc8e744a15c..0acf2b96b74 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -31,6 +31,7 @@ RSpec.describe Group do it { is_expected.to have_one(:dependency_proxy_setting) } it { is_expected.to have_many(:dependency_proxy_blobs) } it { is_expected.to have_many(:dependency_proxy_manifests) } + it { is_expected.to have_many(:debian_distributions).class_name('Packages::Debian::GroupDistribution').dependent(:destroy) } describe '#members & #requesters' do let(:requester) { create(:user) } @@ -1751,4 +1752,23 @@ RSpec.describe Group do it { is_expected.to eq(false) } end end + + describe 'with Debian Distributions' do + subject { create(:group) } + + let!(:distributions) { create_list(:debian_group_distribution, 2, :with_file, container: subject) } + + it 'removes distribution files on removal' do + distribution_file_paths = distributions.map do |distribution| + distribution.file.path + end + + expect { subject.destroy } + .to change { + distribution_file_paths.select do |path| + File.exist? path + end.length + }.from(distribution_file_paths.length).to(0) + end + end end diff --git a/spec/models/list_spec.rb b/spec/models/list_spec.rb index 37158584062..f0b1bc33e84 100644 --- a/spec/models/list_spec.rb +++ b/spec/models/list_spec.rb @@ -4,6 +4,7 @@ require 'spec_helper' RSpec.describe List do it_behaves_like 'having unique enum values' + it_behaves_like 'boards listable model', :list describe 'relationships' do it { is_expected.to belong_to(:board) } @@ -14,72 +15,6 @@ RSpec.describe List do it { is_expected.to validate_presence_of(:board) } it { is_expected.to validate_presence_of(:label) } it { is_expected.to validate_presence_of(:list_type) } - it { is_expected.to validate_presence_of(:position) } - it { is_expected.to validate_numericality_of(:position).only_integer.is_greater_than_or_equal_to(0) } - - context 'when list_type is set to closed' do - subject { described_class.new(list_type: :closed) } - - it { is_expected.not_to validate_presence_of(:label) } - it { is_expected.not_to validate_presence_of(:position) } - end - end - - describe '#destroy' do - it 'can be destroyed when list_type is set to label' do - subject = create(:list) - - expect(subject.destroy).to be_truthy - end - - it 'can not be destroyed when list_type is set to closed' do - subject = create(:closed_list) - - expect(subject.destroy).to be_falsey - end - end - - describe '#destroyable?' do - it 'returns true when list_type is set to label' do - subject.list_type = :label - - expect(subject).to be_destroyable - end - - it 'returns false when list_type is set to closed' do - subject.list_type = :closed - - expect(subject).not_to be_destroyable - end - end - - describe '#movable?' do - it 'returns true when list_type is set to label' do - subject.list_type = :label - - expect(subject).to be_movable - end - - it 'returns false when list_type is set to closed' do - subject.list_type = :closed - - expect(subject).not_to be_movable - end - end - - describe '#title' do - it 'returns label name when list_type is set to label' do - subject.list_type = :label - subject.label = Label.new(name: 'Development') - - expect(subject.title).to eq 'Development' - end - - it 'returns Closed when list_type is set to closed' do - subject.list_type = :closed - - expect(subject.title).to eq 'Closed' - end end describe '#update_preferences_for' do diff --git a/spec/models/members/group_member_spec.rb b/spec/models/members/group_member_spec.rb index 2b24e2d6455..3d3ed6fc54a 100644 --- a/spec/models/members/group_member_spec.rb +++ b/spec/models/members/group_member_spec.rb @@ -123,4 +123,16 @@ RSpec.describe GroupMember do end end end + + context 'when group member expiration date is updated' do + let_it_be(:group_member) { create(:group_member) } + + it 'emails the user that their group membership expiry has changed' do + expect_next_instance_of(NotificationService) do |notification| + allow(notification).to receive(:updated_group_member_expiration).with(group_member) + end + + group_member.update!(expires_at: 5.days.from_now) + end + end end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 431a60a11a5..1cf197322f5 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -411,6 +411,48 @@ RSpec.describe MergeRequest, factory_default: :keep do end end + describe '.by_squash_commit_sha' do + subject { described_class.by_squash_commit_sha(sha) } + + let(:sha) { '123abc' } + let(:merge_request) { create(:merge_request, :merged, squash_commit_sha: sha) } + + it 'returns merge requests that match the given squash commit' do + is_expected.to eq([merge_request]) + end + end + + describe '.by_related_commit_sha' do + subject { described_class.by_related_commit_sha(sha) } + + context 'when commit is a squash commit' do + let!(:merge_request) { create(:merge_request, :merged, squash_commit_sha: sha) } + let(:sha) { '123abc' } + + it { is_expected.to eq([merge_request]) } + end + + context 'when commit is a part of the merge request' do + let!(:merge_request) { create(:merge_request, :with_diffs) } + let(:sha) { 'b83d6e391c22777fca1ed3012fce84f633d7fed0' } + + it { is_expected.to eq([merge_request]) } + end + + context 'when commit is a merge commit' do + let!(:merge_request) { create(:merge_request, :merged, merge_commit_sha: sha) } + let(:sha) { '123abc' } + + it { is_expected.to eq([merge_request]) } + end + + context 'when commit is not found' do + let(:sha) { '0000' } + + it { is_expected.to be_empty } + end + end + describe '.by_cherry_pick_sha' do it 'returns merge requests that match the given merge commit' do note = create(:track_mr_picking_note, commit_id: '456abc') @@ -3555,112 +3597,6 @@ RSpec.describe MergeRequest, factory_default: :keep do end end - describe '#mergeable_with_quick_action?' do - def create_pipeline(status) - pipeline = create(:ci_pipeline, - project: project, - ref: merge_request.source_branch, - sha: merge_request.diff_head_sha, - status: status, - head_pipeline_of: merge_request) - - pipeline - end - - let_it_be(:project) { create(:project, :public, :repository, only_allow_merge_if_pipeline_succeeds: true) } - - let(:developer) { create(:user) } - let(:user) { create(:user) } - let(:merge_request) { create(:merge_request, source_project: project) } - let(:mr_sha) { merge_request.diff_head_sha } - - before do - project.add_developer(developer) - end - - context 'when autocomplete_precheck is set to true' do - it 'is mergeable by developer' do - expect(merge_request.mergeable_with_quick_action?(developer, autocomplete_precheck: true)).to be_truthy - end - - it 'is not mergeable by normal user' do - expect(merge_request.mergeable_with_quick_action?(user, autocomplete_precheck: true)).to be_falsey - end - end - - context 'when autocomplete_precheck is set to false' do - it 'is mergeable by developer' do - expect(merge_request.mergeable_with_quick_action?(developer, last_diff_sha: mr_sha)).to be_truthy - end - - it 'is not mergeable by normal user' do - expect(merge_request.mergeable_with_quick_action?(user, last_diff_sha: mr_sha)).to be_falsey - end - - context 'closed MR' do - before do - merge_request.update_attribute(:state_id, described_class.available_states[:closed]) - end - - it 'is not mergeable' do - expect(merge_request.mergeable_with_quick_action?(developer, last_diff_sha: mr_sha)).to be_falsey - end - end - - context 'MR with WIP' do - before do - merge_request.update_attribute(:title, 'WIP: some MR') - end - - it 'is not mergeable' do - expect(merge_request.mergeable_with_quick_action?(developer, last_diff_sha: mr_sha)).to be_falsey - end - end - - context 'sha differs from the MR diff_head_sha' do - it 'is not mergeable' do - expect(merge_request.mergeable_with_quick_action?(developer, last_diff_sha: 'some other sha')).to be_falsey - end - end - - context 'sha is not provided' do - it 'is not mergeable' do - expect(merge_request.mergeable_with_quick_action?(developer)).to be_falsey - end - end - - context 'with pipeline ok' do - before do - create_pipeline(:success) - end - - it 'is mergeable' do - expect(merge_request.mergeable_with_quick_action?(developer, last_diff_sha: mr_sha)).to be_truthy - end - end - - context 'with failing pipeline' do - before do - create_pipeline(:failed) - end - - it 'is not mergeable' do - expect(merge_request.mergeable_with_quick_action?(developer, last_diff_sha: mr_sha)).to be_falsey - end - end - - context 'with running pipeline' do - before do - create_pipeline(:running) - end - - it 'is mergeable' do - expect(merge_request.mergeable_with_quick_action?(developer, last_diff_sha: mr_sha)).to be_truthy - end - end - end - end - describe '#pipeline_coverage_delta' do let!(:merge_request) { create(:merge_request) } diff --git a/spec/models/namespace/package_setting_spec.rb b/spec/models/namespace/package_setting_spec.rb new file mode 100644 index 00000000000..097cef8ef3b --- /dev/null +++ b/spec/models/namespace/package_setting_spec.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Namespace::PackageSetting do + describe 'relationships' do + it { is_expected.to belong_to(:namespace) } + end + + describe 'validations' do + it { is_expected.to validate_presence_of(:namespace) } + + describe '#maven_duplicates_allowed' do + it { is_expected.to allow_value(true).for(:maven_duplicates_allowed) } + it { is_expected.to allow_value(false).for(:maven_duplicates_allowed) } + it { is_expected.not_to allow_value(nil).for(:maven_duplicates_allowed) } + end + + describe '#maven_duplicate_exception_regex' do + let_it_be(:package_settings) { create(:namespace_package_setting) } + + subject { package_settings } + + valid_regexps = %w[SNAPSHOT .* v.+ v10.1.* (?:v.+|SNAPSHOT|TEMP)] + invalid_regexps = ['[', '(?:v.+|SNAPSHOT|TEMP'] + + valid_regexps.each do |valid_regexp| + it { is_expected.to allow_value(valid_regexp).for(:maven_duplicate_exception_regex) } + end + + invalid_regexps.each do |invalid_regexp| + it { is_expected.not_to allow_value(invalid_regexp).for(:maven_duplicate_exception_regex) } + end + end + end + + describe '#duplicates_allowed?' do + using RSpec::Parameterized::TableSyntax + + subject { described_class.duplicates_allowed?(package) } + + context 'package types with package_settings' do + # As more package types gain settings they will be added to this list + [:maven_package].each do |format| + let_it_be(:package) { create(format) } # rubocop:disable Rails/SaveBang + let_it_be(:package_type) { package.package_type } + let_it_be(:package_setting) { package.project.namespace.package_settings } + + where(:duplicates_allowed, :duplicate_exception_regex, :result) do + true | '' | true + false | '' | false + false | '.*' | true + end + + with_them do + context "for #{format}" do + before do + package_setting.update!( + "#{package_type}_duplicates_allowed" => duplicates_allowed, + "#{package_type}_duplicate_exception_regex" => duplicate_exception_regex + ) + end + + it { is_expected.to be(result) } + end + end + end + end + + context 'package types without package_settings' do + [:npm_package, :conan_package, :nuget_package, :pypi_package, :composer_package, :generic_package, :golang_package, :debian_package].each do |format| + let_it_be(:package) { create(format) } # rubocop:disable Rails/SaveBang + let_it_be(:package_setting) { package.project.namespace.package_settings } + + it 'raises an error' do + expect { subject }.to raise_error(Namespace::PackageSetting::PackageSettingNotImplemented) + end + end + end + end +end diff --git a/spec/models/namespace_onboarding_action_spec.rb b/spec/models/namespace_onboarding_action_spec.rb deleted file mode 100644 index 70dcb989b32..00000000000 --- a/spec/models/namespace_onboarding_action_spec.rb +++ /dev/null @@ -1,53 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe NamespaceOnboardingAction do - let(:namespace) { build(:namespace) } - - describe 'associations' do - it { is_expected.to belong_to(:namespace).required } - end - - describe 'validations' do - it { is_expected.to validate_presence_of(:action) } - end - - describe '.completed?' do - let(:action) { :subscription_created } - - subject { described_class.completed?(namespace, action) } - - context 'action created for the namespace' do - before do - create(:namespace_onboarding_action, namespace: namespace, action: action) - end - - it { is_expected.to eq(true) } - end - - context 'action created for another namespace' do - before do - create(:namespace_onboarding_action, namespace: build(:namespace), action: action) - end - - it { is_expected.to eq(false) } - end - end - - describe '.create_action' do - let(:action) { :subscription_created } - - subject(:create_action) { described_class.create_action(namespace, action) } - - it 'creates the action for the namespace just once' do - expect { create_action }.to change { count_namespace_actions }.by(1) - - expect { create_action }.to change { count_namespace_actions }.by(0) - end - - def count_namespace_actions - described_class.where(namespace: namespace, action: action).count - end - end -end diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index 0130618d004..a3c0a43115e 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -19,7 +19,8 @@ RSpec.describe Namespace do it { is_expected.to have_one :aggregation_schedule } it { is_expected.to have_one :namespace_settings } it { is_expected.to have_many :custom_emoji } - it { is_expected.to have_many :namespace_onboarding_actions } + it { is_expected.to have_one :package_setting_relation } + it { is_expected.to have_one :onboarding_progress } end describe 'validations' do @@ -1500,4 +1501,24 @@ RSpec.describe Namespace do end end end + + describe '#root?' do + subject { namespace.root? } + + context 'when is subgroup' do + before do + namespace.parent = build(:group) + end + + it 'returns false' do + is_expected.to eq(false) + end + end + + context 'when is root' do + it 'returns true' do + is_expected.to eq(true) + end + end + end end diff --git a/spec/models/onboarding_progress_spec.rb b/spec/models/onboarding_progress_spec.rb new file mode 100644 index 00000000000..bd951846bb8 --- /dev/null +++ b/spec/models/onboarding_progress_spec.rb @@ -0,0 +1,107 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe OnboardingProgress do + let(:namespace) { create(:namespace) } + let(:action) { :subscription_created } + + describe 'associations' do + it { is_expected.to belong_to(:namespace).required } + end + + describe 'validations' do + describe 'namespace_is_root_namespace' do + subject(:onboarding_progress) { build(:onboarding_progress, namespace: namespace)} + + context 'when associated namespace is root' do + it { is_expected.to be_valid } + end + + context 'when associated namespace is not root' do + let(:namespace) { build(:group, :nested) } + + it 'is invalid' do + expect(onboarding_progress).to be_invalid + expect(onboarding_progress.errors[:namespace]).to include('must be a root namespace') + end + end + end + end + + describe '.onboard' do + subject(:onboard) { described_class.onboard(namespace) } + + it 'adds a record for the namespace' do + expect { onboard }.to change(described_class, :count).from(0).to(1) + end + + context 'when not given a namespace' do + let(:namespace) { nil } + + it 'does not add a record for the namespace' do + expect { onboard }.not_to change(described_class, :count).from(0) + end + end + + context 'when not given a root namespace' do + let(:namespace) { create(:namespace, parent: build(:namespace)) } + + it 'does not add a record for the namespace' do + expect { onboard }.not_to change(described_class, :count).from(0) + end + end + end + + describe '.register' do + subject(:register_action) { described_class.register(namespace, action) } + + context 'when the namespace was onboarded' do + before do + described_class.onboard(namespace) + end + + it 'registers the action for the namespace' do + expect { register_action }.to change { described_class.completed?(namespace, action) }.from(false).to(true) + end + + context 'when the action does not exist' do + let(:action) { :foo } + + it 'does not register the action for the namespace' do + expect { register_action }.not_to change { described_class.completed?(namespace, action) }.from(nil) + end + end + end + + context 'when the namespace was not onboarded' do + it 'does not register the action for the namespace' do + expect { register_action }.not_to change { described_class.completed?(namespace, action) }.from(false) + end + end + end + + describe '.completed?' do + subject { described_class.completed?(namespace, action) } + + context 'when the namespace has not yet been onboarded' do + it { is_expected.to eq(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 eq(false) } + + context 'when the action has been registered' do + before do + described_class.register(namespace, action) + end + + it { is_expected.to eq(true) } + end + end + end +end diff --git a/spec/models/packages/debian/file_metadatum_spec.rb b/spec/models/packages/debian/file_metadatum_spec.rb new file mode 100644 index 00000000000..1215adfa6a1 --- /dev/null +++ b/spec/models/packages/debian/file_metadatum_spec.rb @@ -0,0 +1,91 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Packages::Debian::FileMetadatum, type: :model do + RSpec.shared_context 'Debian file metadatum' do |factory, trait| + let_it_be_with_reload(:debian_package_file) { create(factory, trait) } + let(:debian_file_metadatum) { debian_package_file.debian_file_metadatum } + + subject { debian_file_metadatum } + end + + RSpec.shared_examples 'Test Debian file metadatum' do |has_component, has_architecture, has_fields, has_outdated| + describe 'relationships' do + it { is_expected.to belong_to(:package_file) } + end + + describe 'validations' do + describe '#package_file' do + it { is_expected.to validate_presence_of(:package_file) } + end + + describe '#file_type' do + it { is_expected.to validate_presence_of(:file_type) } + end + + describe '#component' do + it "has_component=#{has_component}" do + if has_component + is_expected.to validate_presence_of(:component) + is_expected.to allow_value('main').for(:component) + is_expected.not_to allow_value('hé').for(:component) + else + is_expected.to validate_absence_of(:component) + end + end + end + + describe '#architecture' do + it "has_architecture=#{has_architecture}" do + if has_architecture + is_expected.to validate_presence_of(:architecture) + is_expected.to allow_value('amd64').for(:architecture) + is_expected.not_to allow_value('-a').for(:architecture) + else + is_expected.to validate_absence_of(:architecture) + end + end + end + + describe '#fields' do + if has_fields + it { is_expected.to validate_presence_of(:fields) } + it { is_expected.to allow_value({ 'a': 'b' }).for(:fields) } + it { is_expected.not_to allow_value({ 'a': { 'b': 'c' } }).for(:fields) } + else + it { is_expected.to validate_absence_of(:fields) } + end + end + + describe '#debian_package_type' do + before do + debian_package_file.package.package_type = :pypi + end + + it 'validates package of type debian' do + expect(debian_file_metadatum).not_to be_valid + expect(debian_file_metadatum.errors.to_a).to contain_exactly('Package file Package type must be Debian') + end + end + end + end + + using RSpec::Parameterized::TableSyntax + + where(:factory, :trait, :has_component, :has_architecture, :has_fields) do + :debian_package_file | :unknown | false | false | false + :debian_package_file | :source | true | false | false + :debian_package_file | :dsc | true | false | true + :debian_package_file | :deb | true | true | true + :debian_package_file | :udeb | true | true | true + :debian_package_file | :buildinfo | true | false | true + :debian_package_file | :changes | false | false | true + end + + with_them do + include_context 'Debian file metadatum', params[:factory], params[:trait] do + it_behaves_like 'Test Debian file metadatum', params[:has_component], params[:has_architecture], params[:has_fields], params[:has_outdated] + end + end +end diff --git a/spec/models/packages/debian/group_architecture_spec.rb b/spec/models/packages/debian/group_architecture_spec.rb new file mode 100644 index 00000000000..a6def6e56cd --- /dev/null +++ b/spec/models/packages/debian/group_architecture_spec.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Packages::Debian::GroupArchitecture do + it_behaves_like 'Debian Distribution Architecture', :debian_group_architecture, :group, false +end diff --git a/spec/models/packages/debian/group_distribution_spec.rb b/spec/models/packages/debian/group_distribution_spec.rb new file mode 100644 index 00000000000..90fb0d0e7d8 --- /dev/null +++ b/spec/models/packages/debian/group_distribution_spec.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Packages::Debian::GroupDistribution do + it_behaves_like 'Debian Distribution', :debian_group_distribution, :group, false +end diff --git a/spec/models/packages/debian/project_architecture_spec.rb b/spec/models/packages/debian/project_architecture_spec.rb new file mode 100644 index 00000000000..b82ecabc4d4 --- /dev/null +++ b/spec/models/packages/debian/project_architecture_spec.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Packages::Debian::ProjectArchitecture do + it_behaves_like 'Debian Distribution Architecture', :debian_project_architecture, :project, true +end diff --git a/spec/models/packages/debian/project_distribution_spec.rb b/spec/models/packages/debian/project_distribution_spec.rb new file mode 100644 index 00000000000..5f4041ad9fe --- /dev/null +++ b/spec/models/packages/debian/project_distribution_spec.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Packages::Debian::ProjectDistribution do + it_behaves_like 'Debian Distribution', :debian_project_distribution, :project, true +end diff --git a/spec/models/packages/package_file_spec.rb b/spec/models/packages/package_file_spec.rb index 82ac159b9cc..ebb10e991ad 100644 --- a/spec/models/packages/package_file_spec.rb +++ b/spec/models/packages/package_file_spec.rb @@ -7,6 +7,7 @@ RSpec.describe Packages::PackageFile, type: :model do it { is_expected.to have_one(:conan_file_metadatum) } it { is_expected.to have_many(:package_file_build_infos).inverse_of(:package_file) } it { is_expected.to have_many(:pipelines).through(:package_file_build_infos) } + it { is_expected.to have_one(:debian_file_metadatum).inverse_of(:package_file).class_name('Packages::Debian::FileMetadatum') } end describe 'validations' do diff --git a/spec/models/packages/package_spec.rb b/spec/models/packages/package_spec.rb index 16764673474..6645db33503 100644 --- a/spec/models/packages/package_spec.rb +++ b/spec/models/packages/package_spec.rb @@ -599,6 +599,20 @@ RSpec.describe Packages::Package, type: :model do end end + describe '.order_by_package_file' do + let_it_be(:project) { create(:project) } + let_it_be(:package1) { create(:maven_package, project: project) } + let_it_be(:package2) { create(:maven_package, project: project) } + + it 'orders packages their associated package_file\'s created_at date', :aggregate_failures do + expect(project.packages.order_by_package_file).to match_array([package1, package1, package1, package2, package2, package2]) + + create(:package_file, :xml, package: package1) + + expect(project.packages.order_by_package_file).to match_array([package1, package1, package1, package2, package2, package2, package1]) + end + end + describe '#versions' do let_it_be(:project) { create(:project) } let_it_be(:package) { create(:maven_package, project: project) } @@ -731,4 +745,14 @@ RSpec.describe Packages::Package, type: :model do end end end + + describe '#package_settings' do + let_it_be(:group) { create(:group) } + let_it_be(:project) { create(:project, group: group) } + let_it_be(:package) { create(:maven_package, project: project) } + + it 'returns the namespace package_settings' do + expect(package.package_settings).to eq(group.package_settings) + end + end end diff --git a/spec/models/project_import_state_spec.rb b/spec/models/project_import_state_spec.rb index 6a0402d43a8..843beb4ce23 100644 --- a/spec/models/project_import_state_spec.rb +++ b/spec/models/project_import_state_spec.rb @@ -103,7 +103,7 @@ RSpec.describe ProjectImportState, type: :model do allow(after_import_service) .to receive(:execute) { housekeeping_service.execute } - allow(Projects::HousekeepingService) + allow(Repositories::HousekeepingService) .to receive(:new) { housekeeping_service } end diff --git a/spec/models/project_pages_metadatum_spec.rb b/spec/models/project_pages_metadatum_spec.rb new file mode 100644 index 00000000000..31a533e0363 --- /dev/null +++ b/spec/models/project_pages_metadatum_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ProjectPagesMetadatum do + describe '.only_on_legacy_storage' do + it 'returns only deployed records without deployment' do + create(:project) # without pages deployed + + legacy_storage_project = create(:project) + legacy_storage_project.mark_pages_as_deployed + + project_with_deployment = create(:project) + deployment = create(:pages_deployment, project: project_with_deployment) + project_with_deployment.mark_pages_as_deployed + project_with_deployment.update_pages_deployment!(deployment) + + expect(described_class.only_on_legacy_storage).to eq([legacy_storage_project.pages_metadatum]) + end + end +end diff --git a/spec/models/project_services/alerts_service_spec.rb b/spec/models/project_services/alerts_service_spec.rb index db25885c76a..75b91c29914 100644 --- a/spec/models/project_services/alerts_service_spec.rb +++ b/spec/models/project_services/alerts_service_spec.rb @@ -2,108 +2,38 @@ require 'spec_helper' +# AlertsService is stripped down to only required methods +# to avoid errors loading integration-related pages if +# records are present. RSpec.describe AlertsService do let_it_be(:project) { create(:project) } - let(:service_params) { { project: project, active: active } } - let(:active) { true } - let(:service) { described_class.new(service_params) } + subject(:service) { described_class.new(project: project) } - shared_context 'when active' do - let(:active) { true } - end - - shared_context 'when inactive' do - let(:active) { false } - end - - shared_context 'when persisted' do - before do - service.save! - service.reload - end - end - - describe '#url' do - include Gitlab::Routing - - subject { service.url } + it { is_expected.to be_valid } - it { is_expected.to eq(project_alerts_notify_url(project, format: :json)) } - end - - describe '#json_fields' do - subject { service.json_fields } + describe '#to_param' do + subject { service.to_param } - it { is_expected.to eq(%w(active token)) } + it { is_expected.to eq('alerts') } end - describe '#as_json' do - subject { service.as_json(only: service.json_fields) } + describe '#supported_events' do + subject { service.supported_events } - it { is_expected.to eq('active' => true, 'token' => nil) } + it { is_expected.to be_empty } end - describe '#token' do - shared_context 'reset token' do - before do - service.token = '' - service.valid? - end - end - - shared_context 'assign token' do |token| - before do - service.token = token - service.valid? - end - end - - shared_examples 'valid token' do - it { is_expected.to match(/\A\h{32}\z/) } - end - - shared_examples 'no token' do - it { is_expected.to be_blank } - end - - subject { service.token } - - context 'when active' do - include_context 'when active' - - context 'when resetting' do - let!(:previous_token) { service.token } - - include_context 'reset token' - - it_behaves_like 'valid token' - - it { is_expected.not_to eq(previous_token) } - end - - context 'when assigning' do - include_context 'assign token', 'random token' - - it_behaves_like 'valid token' - end - end - - context 'when inactive' do - include_context 'when inactive' - - context 'when resetting' do - let!(:previous_token) { service.token } - - include_context 'reset token' - - it_behaves_like 'no token' - end - end + describe '#save' do + it 'prevents records from being created or updated' do + expect(Gitlab::ProjectServiceLogger).to receive(:error).with( + hash_including(message: 'Prevented attempt to save or update deprecated AlertsService') + ) - context 'when persisted' do - include_context 'when persisted' + expect(service.save).to be_falsey - it_behaves_like 'valid token' + expect(service.errors.full_messages).to include( + 'Alerts endpoint is deprecated and should not be created or modified. Use HTTP Integrations instead.' + ) end end end diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb index e7cd3d7f537..cd0873bddd2 100644 --- a/spec/models/project_services/jira_service_spec.rb +++ b/spec/models/project_services/jira_service_spec.rb @@ -5,12 +5,21 @@ require 'spec_helper' RSpec.describe JiraService do include AssetsHelpers + let_it_be(:project) { create(:project, :repository) } let(:url) { 'http://jira.example.com' } let(:api_url) { 'http://api-jira.example.com' } let(:username) { 'jira-username' } let(:password) { 'jira-password' } let(:transition_id) { 'test27' } let(:server_info_results) { { 'deploymentType' => 'Cloud' } } + let(:jira_service) do + described_class.new( + project: project, + url: url, + username: username, + password: password + ) + end before do WebMock.stub_request(:get, /serverInfo/).to_return(body: server_info_results.to_json ) @@ -19,7 +28,7 @@ RSpec.describe JiraService do describe '#options' do let(:options) do { - project: create(:project), + project: project, active: true, username: 'username', password: 'test', @@ -108,7 +117,7 @@ RSpec.describe JiraService do describe '#create' do let(:params) do { - project: create(:project), + project: project, url: url, api_url: api_url, username: username, password: password, jira_issue_transition_id: transition_id @@ -434,10 +443,23 @@ RSpec.describe JiraService do end end + describe '#find_issue' do + let(:issue_key) { 'JIRA-123' } + let(:issue_url) { "#{url}/rest/api/2/issue/#{issue_key}" } + + before do + stub_request(:get, issue_url).with(basic_auth: [username, password]) + end + + it 'call the Jira API to get the issue' do + jira_service.find_issue(issue_key) + + expect(WebMock).to have_requested(:get, issue_url) + end + end + describe '#close_issue' do let(:custom_base_url) { 'http://custom_url' } - let(:user) { create(:user) } - let(:project) { create(:project, :repository) } shared_examples 'close_issue' do before do @@ -445,7 +467,6 @@ RSpec.describe JiraService do allow(@jira_service).to receive_messages( project_id: project.id, project: project, - service_hook: true, url: 'http://jira.example.com', username: 'gitlab_jira_username', password: 'gitlab_jira_password', @@ -657,17 +678,7 @@ RSpec.describe JiraService do end describe '#create_cross_reference_note' do - let_it_be(:user) { build_stubbed(:user) } - let_it_be(:project) { create(:project, :repository) } - let(:jira_service) do - described_class.new( - project: project, - url: url, - username: username, - password: password - ) - end - + let_it_be(:user) { build_stubbed(:user) } let(:jira_issue) { ExternalIssue.new('JIRA-123', project) } subject { jira_service.create_cross_reference_note(jira_issue, resource, user) } @@ -732,15 +743,6 @@ RSpec.describe JiraService do describe '#test' do let(:server_info_results) { { 'url' => 'http://url', 'deploymentType' => 'Cloud' } } - let_it_be(:project) { create(:project, :repository) } - let(:jira_service) do - described_class.new( - url: url, - project: project, - username: username, - password: password - ) - end def server_info jira_service.test(nil) @@ -790,7 +792,6 @@ RSpec.describe JiraService do } allow(Gitlab.config).to receive(:issues_tracker).and_return(settings) - project = create(:project) service = project.create_jira_service(active: true) expect(service.url).to eq('http://jira.sample/projects/project_a') diff --git a/spec/models/project_services/prometheus_service_spec.rb b/spec/models/project_services/prometheus_service_spec.rb index 76fc5a826c9..8215fb5c336 100644 --- a/spec/models/project_services/prometheus_service_spec.rb +++ b/spec/models/project_services/prometheus_service_spec.rb @@ -95,7 +95,7 @@ RSpec.describe PrometheusService, :use_clean_rails_memory_store_caching, :snowpl service.api_url = 'http://localhost:9090' stub_application_setting(self_monitoring_project_id: project.id) - stub_config(prometheus: { enable: true, listen_address: 'localhost:9090' }) + stub_config(prometheus: { enable: true, server_address: 'localhost:9090' }) end it 'allows self-monitoring project to connect to internal Prometheus' do @@ -242,7 +242,7 @@ RSpec.describe PrometheusService, :use_clean_rails_memory_store_caching, :snowpl stub_config(prometheus: { enable: true, - listen_address: api_url + server_address: api_url }) end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index a71b0eb842a..a2b51684d4d 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -127,6 +127,7 @@ RSpec.describe Project, factory_default: :keep do it { is_expected.to have_many(:reviews).inverse_of(:project) } it { is_expected.to have_many(:packages).class_name('Packages::Package') } it { is_expected.to have_many(:package_files).class_name('Packages::PackageFile') } + it { is_expected.to have_many(:debian_distributions).class_name('Packages::Debian::ProjectDistribution').dependent(:destroy) } it { is_expected.to have_many(:pipeline_artifacts) } it { is_expected.to have_many(:terraform_states).class_name('Terraform::State').inverse_of(:project) } @@ -1066,36 +1067,6 @@ RSpec.describe Project, factory_default: :keep do end end - describe '#cache_has_external_wiki' do - let_it_be(:project) { create(:project, has_external_wiki: nil) } - - it 'stores true if there is any external_wikis' do - services = double(:service, external_wikis: [ExternalWikiService.new]) - expect(project).to receive(:services).and_return(services) - - expect do - project.cache_has_external_wiki - end.to change { project.has_external_wiki}.to(true) - end - - it 'stores false if there is no external_wikis' do - services = double(:service, external_wikis: []) - expect(project).to receive(:services).and_return(services) - - expect do - project.cache_has_external_wiki - end.to change { project.has_external_wiki}.to(false) - end - - it 'does not cache data when in a read-only GitLab instance' do - allow(Gitlab::Database).to receive(:read_only?) { true } - - expect do - project.cache_has_external_wiki - end.not_to change { project.has_external_wiki } - end - end - describe '#has_wiki?' do let(:no_wiki_project) { create(:project, :wiki_disabled, has_external_wiki: false) } let(:wiki_enabled_project) { create(:project) } @@ -1135,52 +1106,64 @@ RSpec.describe Project, factory_default: :keep do describe '#external_wiki' do let_it_be(:project) { create(:project) } - context 'with an active external wiki' do - before do - create(:service, project: project, type: 'ExternalWikiService', active: true) - project.external_wiki - end + def subject + project.reload.external_wiki + end - it 'sets :has_external_wiki as true' do - expect(project.has_external_wiki).to be(true) - end + it 'returns an active external wiki' do + create(:service, project: project, type: 'ExternalWikiService', active: true) - it 'sets :has_external_wiki as false if an external wiki service is destroyed later' do - expect(project.has_external_wiki).to be(true) + is_expected.to be_kind_of(ExternalWikiService) + end - project.services.external_wikis.first.destroy + it 'does not return an inactive external wiki' do + create(:service, project: project, type: 'ExternalWikiService', active: false) - expect(project.has_external_wiki).to be(false) - end + is_expected.to eq(nil) end - context 'with an inactive external wiki' do - before do - create(:service, project: project, type: 'ExternalWikiService', active: false) - end + it 'sets Project#has_external_wiki when it is nil' do + create(:service, project: project, type: 'ExternalWikiService', active: true) + project.update_column(:has_external_wiki, nil) - it 'sets :has_external_wiki as false' do - expect(project.has_external_wiki).to be(false) - end + expect { subject }.to change { project.has_external_wiki }.from(nil).to(true) end + end - context 'with no external wiki' do - before do - project.external_wiki - end + describe '#has_external_wiki' do + let_it_be(:project) { create(:project) } - it 'sets :has_external_wiki as false' do - expect(project.has_external_wiki).to be(false) - end + def subject + project.reload.has_external_wiki + end - it 'sets :has_external_wiki as true if an external wiki service is created later' do - expect(project.has_external_wiki).to be(false) + specify { is_expected.to eq(false) } + context 'when there is an active external wiki service' do + let!(:service) do create(:service, project: project, type: 'ExternalWikiService', active: true) + end + + specify { is_expected.to eq(true) } + + it 'becomes false if the external wiki service is destroyed' do + expect do + Service.find(service.id).delete + end.to change { subject }.to(false) + end - expect(project.has_external_wiki).to be(true) + it 'becomes false if the external wiki service becomes inactive' do + expect do + service.update_column(:active, false) + end.to change { subject }.to(false) end end + + it 'is false when external wiki service is not active' do + create(:service, project: project, type: 'ExternalWikiService', active: false) + + is_expected.to eq(false) + end end describe '#star_count' do @@ -1516,63 +1499,13 @@ RSpec.describe Project, factory_default: :keep do allow(::Gitlab::ServiceDeskEmail).to receive(:config).and_return(config) end - context 'when service_desk_custom_address flag is enabled' do - before do - stub_feature_flags(service_desk_custom_address: true) - end - - it 'returns custom address when project_key is set' do - create(:service_desk_setting, project: project, project_key: 'key1') - - expect(subject).to eq("foo+#{project.full_path_slug}-key1@bar.com") - end + it 'returns custom address when project_key is set' do + create(:service_desk_setting, project: project, project_key: 'key1') - it_behaves_like 'with incoming email address' + expect(subject).to eq("foo+#{project.full_path_slug}-key1@bar.com") end - context 'when service_desk_custom_address flag is disabled' do - before do - stub_feature_flags(service_desk_custom_address: false) - end - - it_behaves_like 'with incoming email address' - end - end - end - - describe '.service_desk_custom_address_enabled?' do - let_it_be(:project) { create(:project, service_desk_enabled: true) } - - subject(:address_enabled) { project.service_desk_custom_address_enabled? } - - context 'when service_desk_email is enabled' do - before do - allow(::Gitlab::ServiceDeskEmail).to receive(:enabled?).and_return(true) - end - - it 'returns true' do - expect(address_enabled).to be_truthy - end - - context 'when service_desk_custom_address flag is disabled' do - before do - stub_feature_flags(service_desk_custom_address: false) - end - - it 'returns false' do - expect(address_enabled).to be_falsey - end - end - end - - context 'when service_desk_email is disabled' do - before do - allow(::Gitlab::ServiceDeskEmail).to receive(:enabled?).and_return(false) - end - - it 'returns false when service_desk_email is disabled' do - expect(address_enabled).to be_falsey - end + it_behaves_like 'with incoming email address' end end @@ -3044,56 +2977,9 @@ RSpec.describe Project, factory_default: :keep do end end - describe '#pushes_since_gc' do - let(:project) { build_stubbed(:project) } - - after do - project.reset_pushes_since_gc - end - - context 'without any pushes' do - it 'returns 0' do - expect(project.pushes_since_gc).to eq(0) - end - end - - context 'with a number of pushes' do - it 'returns the number of pushes' do - 3.times { project.increment_pushes_since_gc } - - expect(project.pushes_since_gc).to eq(3) - end - end - end - - describe '#increment_pushes_since_gc' do - let(:project) { build_stubbed(:project) } - - after do - project.reset_pushes_since_gc - end - - it 'increments the number of pushes since the last GC' do - 3.times { project.increment_pushes_since_gc } - - expect(project.pushes_since_gc).to eq(3) - end - end - - describe '#reset_pushes_since_gc' do - let(:project) { build_stubbed(:project) } - - after do - project.reset_pushes_since_gc - end - - it 'resets the number of pushes since the last GC' do - 3.times { project.increment_pushes_since_gc } - - project.reset_pushes_since_gc - - expect(project.pushes_since_gc).to eq(0) - end + it_behaves_like 'can housekeep repository' do + let(:resource) { build_stubbed(:project) } + let(:resource_key) { 'projects' } end describe '#deployment_variables' do @@ -4548,6 +4434,24 @@ RSpec.describe Project, factory_default: :keep do end end + describe '#predefined_project_variables' do + let_it_be(:project) { create(:project, :repository) } + + 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 }) + end + + context 'when ci config path is overridden' do + before 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 }) } + end + end + describe '#auto_devops_enabled?' do before do Feature.enable_percentage_of_actors(:force_autodevops_on_by_default, 0) @@ -6002,6 +5906,43 @@ RSpec.describe Project, factory_default: :keep do end end + describe '#set_first_pages_deployment!' do + let(:project) { create(:project) } + let(:deployment) { create(:pages_deployment, project: project) } + + it "creates new metadata record if none exists yet and sets deployment" do + project.pages_metadatum.destroy! + project.reload + + project.set_first_pages_deployment!(deployment) + + expect(project.pages_metadatum.reload.pages_deployment).to eq(deployment) + end + + it "updates the existing metadara record with deployment" do + expect do + project.set_first_pages_deployment!(deployment) + end.to change { project.pages_metadatum.reload.pages_deployment }.from(nil).to(deployment) + end + + it 'only updates metadata for this project' do + other_project = create(:project) + + expect do + project.set_first_pages_deployment!(deployment) + end.not_to change { other_project.pages_metadatum.reload.pages_deployment }.from(nil) + end + + it 'does nothing if metadata already references some deployment' do + existing_deployment = create(:pages_deployment, project: project) + project.set_first_pages_deployment!(existing_deployment) + + expect do + project.set_first_pages_deployment!(deployment) + end.not_to change { project.pages_metadatum.reload.pages_deployment }.from(existing_deployment) + end + end + describe '#has_pool_repsitory?' do it 'returns false when it does not have a pool repository' do subject = create(:project, :repository) @@ -6275,28 +6216,6 @@ RSpec.describe Project, factory_default: :keep do end end - describe '#alerts_service_activated?' do - let!(:project) { create(:project) } - - subject { project.alerts_service_activated? } - - context 'when project has an activated alerts service' do - before do - create(:alerts_service, project: project) - end - - it { is_expected.to be_truthy } - end - - context 'when project has an inactive alerts service' do - before do - create(:alerts_service, :inactive, project: project) - end - - it { is_expected.to be_falsey } - end - end - describe '#prometheus_service_active?' do let(:project) { create(:project) } @@ -6456,6 +6375,25 @@ RSpec.describe Project, factory_default: :keep do end end + describe 'with Debian Distributions' do + subject { create(:project) } + + let!(:distributions) { create_list(:debian_project_distribution, 2, :with_file, container: subject) } + + it 'removes distribution files on removal' do + distribution_file_paths = distributions.map do |distribution| + distribution.file.path + end + + expect { subject.destroy } + .to change { + distribution_file_paths.select do |path| + File.exist? path + end.length + }.from(distribution_file_paths.length).to(0) + end + end + describe '#environments_for_scope' do let_it_be(:project, reload: true) { create(:project) } diff --git a/spec/models/project_wiki_spec.rb b/spec/models/project_wiki_spec.rb index 2e82fcf5511..8001d009901 100644 --- a/spec/models/project_wiki_spec.rb +++ b/spec/models/project_wiki_spec.rb @@ -42,4 +42,10 @@ RSpec.describe ProjectWiki do end end end + + it_behaves_like 'can housekeep repository' do + let_it_be(:resource) { create(:project_wiki) } + + let(:resource_key) { 'project_wikis' } + end end diff --git a/spec/models/protectable_dropdown_spec.rb b/spec/models/protectable_dropdown_spec.rb index c51197234ca..918c3078405 100644 --- a/spec/models/protectable_dropdown_spec.rb +++ b/spec/models/protectable_dropdown_spec.rb @@ -14,21 +14,33 @@ RSpec.describe ProtectableDropdown do end describe '#protectable_ref_names' do - before do - project.protected_branches.create(name: 'master') - end + context 'when project repository is not empty' do + before do + project.protected_branches.create(name: 'master') + end + + it { expect(subject.protectable_ref_names).to include('feature') } + it { expect(subject.protectable_ref_names).not_to include('master') } + + it "includes branches matching a protected branch wildcard" do + expect(subject.protectable_ref_names).to include('feature') - it { expect(subject.protectable_ref_names).to include('feature') } - it { expect(subject.protectable_ref_names).not_to include('master') } + create(:protected_branch, name: 'feat*', project: project) - it "includes branches matching a protected branch wildcard" do - expect(subject.protectable_ref_names).to include('feature') + subject = described_class.new(project.reload, :branches) + + expect(subject.protectable_ref_names).to include('feature') + end + end - create(:protected_branch, name: 'feat*', project: project) + context 'when project repository is empty' do + let(:project) { create(:project) } - subject = described_class.new(project.reload, :branches) + it "returns empty list" do + subject = described_class.new(project, :branches) - expect(subject.protectable_ref_names).to include('feature') + expect(subject.protectable_ref_names).to be_empty + end end end end diff --git a/spec/models/release_highlight_spec.rb b/spec/models/release_highlight_spec.rb index ac252e6d6cf..749b9b8e1ab 100644 --- a/spec/models/release_highlight_spec.rb +++ b/spec/models/release_highlight_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' RSpec.describe ReleaseHighlight do - let(:fixture_dir_glob) { Dir.glob(File.join('spec', 'fixtures', 'whats_new', '*.yml')) } + let(:fixture_dir_glob) { Dir.glob(File.join('spec', 'fixtures', 'whats_new', '*.yml')).grep(/\d*\_(\d*\_\d*)\.yml$/) } before do allow(Dir).to receive(:glob).with(Rails.root.join('data', 'whats_new', '*.yml')).and_return(fixture_dir_glob) diff --git a/spec/models/release_spec.rb b/spec/models/release_spec.rb index fea15ea00c8..b436c2e1088 100644 --- a/spec/models/release_spec.rb +++ b/spec/models/release_spec.rb @@ -3,9 +3,9 @@ require 'spec_helper' RSpec.describe Release do - let(:user) { create(:user) } - let(:project) { create(:project, :public, :repository) } - let(:release) { create(:release, project: project, author: user) } + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project, :public, :repository) } + let_it_be(:release) { create(:release, project: project, author: user) } it { expect(release).to be_valid } @@ -132,8 +132,10 @@ RSpec.describe Release do end describe '#milestone_titles' do - let(:release) { create(:release, :with_milestones) } + let_it_be(:milestone_1) { create(:milestone, project: project, title: 'Milestone 1') } + let_it_be(:milestone_2) { create(:milestone, project: project, title: 'Milestone 2') } + let_it_be(:release) { create(:release, project: project, milestones: [milestone_1, milestone_2]) } - it { expect(release.milestone_titles).to eq(release.milestones.map {|m| m.title }.sort.join(", "))} + it { expect(release.milestone_titles).to eq("#{milestone_1.title}, #{milestone_2.title}")} end end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index c1f073e26d1..dd54a701282 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -123,7 +123,7 @@ RSpec.describe Repository do options = { message: 'test tag message\n', tagger: { name: 'John Smith', email: 'john@gmail.com' } } - rugged_repo(repository).tags.create(annotated_tag_name, 'a48e4fc218069f68ef2e769dd8dfea3991362175', options) + rugged_repo(repository).tags.create(annotated_tag_name, 'a48e4fc218069f68ef2e769dd8dfea3991362175', **options) double_first = double(committed_date: Time.current - 1.second) double_last = double(committed_date: Time.current) @@ -2028,6 +2028,22 @@ RSpec.describe Repository do end end + describe '#lookup' do + before do + allow(repository.raw_repository).to receive(:lookup).and_return('interesting_blob') + end + + it 'uses the lookup cache' do + 2.times.each { repository.lookup('sha1') } + + expect(repository.raw_repository).to have_received(:lookup).once + end + + it 'returns the correct value' do + expect(repository.lookup('sha1')).to eq('interesting_blob') + end + end + describe '#after_create' do it 'calls expire_status_cache' do expect(repository).to receive(:expire_status_cache) diff --git a/spec/models/snippet_repository_storage_move_spec.rb b/spec/models/snippet_repository_storage_move_spec.rb index c9feff0c22f..357951f8859 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) { nil } # TODO set to SnippetUpdateRepositoryStorageWorker after https://gitlab.com/gitlab-org/gitlab/-/issues/218991 is implemented + let(:repository_storage_worker) { SnippetUpdateRepositoryStorageWorker } end end diff --git a/spec/models/snippet_spec.rb b/spec/models/snippet_spec.rb index f87259ea048..68d183d5d55 100644 --- a/spec/models/snippet_spec.rb +++ b/spec/models/snippet_spec.rb @@ -796,4 +796,90 @@ RSpec.describe Snippet do it_behaves_like 'can move repository storage' do let_it_be(:container) { create(:snippet, :repository) } end + + describe '#change_head_to_default_branch' do + let(:head_path) { Rails.root.join(TestEnv.repos_path, "#{snippet.disk_path}.git", 'HEAD') } + + subject { snippet.change_head_to_default_branch } + + context 'when repository does not exist' do + let(:snippet) { create(:snippet) } + + it 'does nothing' do + expect(snippet.repository_exists?).to eq false + expect(snippet.repository.raw_repository).not_to receive(:write_ref) + + subject + end + end + + context 'when repository is empty' do + let(:snippet) { create(:snippet, :empty_repo) } + + before do + allow(Gitlab::CurrentSettings).to receive(:default_branch_name).and_return(default_branch) + end + + context 'when default branch in settings is "master"' do + let(:default_branch) { 'master' } + + it 'does nothing' do + expect(File.read(head_path).squish).to eq 'ref: refs/heads/master' + + expect(snippet.repository.raw_repository).not_to receive(:write_ref) + + subject + end + end + + context 'when default branch in settings is different from "master"' do + let(:default_branch) { 'main' } + + it 'changes the HEAD reference to the default branch' do + expect(File.read(head_path).squish).to eq 'ref: refs/heads/master' + + subject + + expect(File.read(head_path).squish).to eq "ref: refs/heads/#{default_branch}" + end + end + end + + context 'when repository is not empty' do + let(:snippet) { create(:snippet, :empty_repo) } + + before do + populate_snippet_repo + end + + context 'when HEAD branch is empty' do + it 'changes HEAD to default branch' do + File.write(head_path, 'ref: refs/heads/non_existen_branch') + expect(File.read(head_path).squish).to eq 'ref: refs/heads/non_existen_branch' + + subject + + expect(File.read(head_path).squish).to eq 'ref: refs/heads/main' + expect(snippet.list_files('HEAD')).not_to be_empty + end + end + + context 'when HEAD branch is not empty' do + it 'does nothing' do + File.write(head_path, 'ref: refs/heads/main') + + expect(snippet.repository.raw_repository).not_to receive(:write_ref) + + subject + end + end + + def populate_snippet_repo + allow(Gitlab::CurrentSettings).to receive(:default_branch_name).and_return('main') + + data = [{ file_path: 'new_file_test', content: 'bar' }] + snippet.snippet_repository.multi_files_action(snippet.author, data, branch_name: 'main', message: 'foo') + end + end + end end diff --git a/spec/models/terraform/state_spec.rb b/spec/models/terraform/state_spec.rb index ef91e4a5a71..ed311314086 100644 --- a/spec/models/terraform/state_spec.rb +++ b/spec/models/terraform/state_spec.rb @@ -27,6 +27,22 @@ RSpec.describe Terraform::State do end end + describe '#destroy' do + let(:terraform_state) { create(:terraform_state) } + let(:user) { terraform_state.project.creator } + + it 'deletes when the state is unlocked' do + expect(terraform_state.destroy).to be_truthy + end + + it 'fails to delete when the state is locked', :aggregate_failures do + terraform_state.update!(lock_xid: SecureRandom.uuid, locked_by_user: user, locked_at: Time.current) + + expect(terraform_state.destroy).to be_falsey + expect(terraform_state.errors.full_messages).to eq(["You cannot remove the State file because it's locked. Unlock the State file first before removing it."]) + end + end + describe '#latest_file' do let(:terraform_state) { create(:terraform_state, :with_version) } let(:latest_version) { terraform_state.latest_version } diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index fb05c9e8052..0935d3576a4 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -4084,6 +4084,7 @@ RSpec.describe User do cache_mock = double expect(cache_mock).to receive(:delete).with(['users', user.id, 'assigned_open_merge_requests_count']) + expect(cache_mock).to receive(:delete).with(['users', user.id, 'review_requested_open_merge_requests_count']) allow(Rails).to receive(:cache).and_return(cache_mock) @@ -4163,6 +4164,20 @@ RSpec.describe User do end end + describe '#review_requested_open_merge_requests_count' do + it 'returns number of open merge requests from non-archived projects' do + user = create(:user) + project = create(:project, :public) + archived_project = create(:project, :public, :archived) + + create(:merge_request, source_project: project, author: user, reviewers: [user]) + create(:merge_request, :closed, source_project: project, author: user, reviewers: [user]) + create(:merge_request, source_project: archived_project, author: user, reviewers: [user]) + + expect(user.review_requested_open_merge_requests_count(force: true)).to eq 1 + end + end + describe '#assigned_open_issues_count' do it 'returns number of open issues from non-archived projects' do user = create(:user) @@ -4265,7 +4280,7 @@ RSpec.describe User do it 'adds the namespace errors to the user' do user.update(username: new_username) - expect(user.errors.full_messages.first).to eq('Username has already been taken') + expect(user.errors.full_messages.first).to eq('A user, alias, or group already exists with that username.') end end end @@ -5074,9 +5089,10 @@ RSpec.describe User do end describe '#hook_attrs' do - it 'includes name, username, avatar_url, and email' do + it 'includes id, name, username, avatar_url, and email' do user = create(:user) user_attributes = { + id: user.id, name: user.name, username: user.username, avatar_url: user.avatar_url(only_path: false), |