diff options
Diffstat (limited to 'spec/models/ci')
20 files changed, 560 insertions, 136 deletions
diff --git a/spec/models/ci/build_report_result_spec.rb b/spec/models/ci/build_report_result_spec.rb index e78f602feef..3f53c6c1c0e 100644 --- a/spec/models/ci/build_report_result_spec.rb +++ b/spec/models/ci/build_report_result_spec.rb @@ -5,6 +5,11 @@ require 'spec_helper' RSpec.describe Ci::BuildReportResult do let(:build_report_result) { build(:ci_build_report_result, :with_junit_success) } + it_behaves_like 'cleanup by a loose foreign key' do + let!(:parent) { create(:project) } + let!(:model) { create(:ci_build_report_result, project: parent) } + end + describe 'associations' do it { is_expected.to belong_to(:build) } it { is_expected.to belong_to(:project) } diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index b9a12339e61..b8c5af5a911 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -565,6 +565,26 @@ RSpec.describe Ci::Build do expect(build.reload.runtime_metadata).not_to be_present end end + + context 'when a failure reason is provided' do + context 'when a failure reason is a symbol' do + it 'correctly sets a failure reason' do + build.drop!(:script_failure) + + expect(build.failure_reason).to eq 'script_failure' + end + end + + context 'when a failure reason is an object' do + it 'correctly sets a failure reason' do + reason = ::Gitlab::Ci::Build::Status::Reason.new(build, :script_failure) + + build.drop!(reason) + + expect(build.failure_reason).to eq 'script_failure' + end + end + end end describe '#schedulable?' do @@ -2002,6 +2022,16 @@ RSpec.describe Ci::Build do it { is_expected.not_to be_retryable } end + + context 'when build is waiting for deployment approval' do + subject { build_stubbed(:ci_build, :manual, environment: 'production') } + + before do + create(:deployment, :blocked, deployable: subject) + end + + it { is_expected.not_to be_retryable } + end end end @@ -2064,6 +2094,31 @@ RSpec.describe Ci::Build do end describe 'build auto retry feature' do + context 'with deployment job' do + let(:build) do + create(:ci_build, :deploy_to_production, :with_deployment, + user: user, pipeline: pipeline, project: project) + end + + before do + project.add_developer(user) + allow(build).to receive(:auto_retry_allowed?) { true } + end + + it 'creates a deployment when a build is dropped' do + expect { build.drop!(:script_failure) }.to change { Deployment.count }.by(1) + + retried_deployment = Deployment.last + expect(build.deployment.environment).to eq(retried_deployment.environment) + expect(build.deployment.ref).to eq(retried_deployment.ref) + expect(build.deployment.sha).to eq(retried_deployment.sha) + expect(build.deployment.tag).to eq(retried_deployment.tag) + expect(build.deployment.user).to eq(retried_deployment.user) + expect(build.deployment).to be_failed + expect(retried_deployment).to be_created + end + end + describe '#retries_count' do subject { create(:ci_build, name: 'test', pipeline: pipeline) } @@ -2152,6 +2207,28 @@ RSpec.describe Ci::Build do end end + describe '#auto_retry_expected?' do + subject { create(:ci_build, :failed) } + + context 'when build is failed and auto retry is configured' do + before do + allow(subject) + .to receive(:auto_retry_allowed?) + .and_return(true) + end + + it 'expects auto-retry to happen' do + expect(subject.auto_retry_expected?).to be true + end + end + + context 'when build failed by auto retry is not configured' do + it 'does not expect auto-retry to happen' do + expect(subject.auto_retry_expected?).to be false + end + end + end + describe '#artifacts_file_for_type' do let(:build) { create(:ci_build, :artifacts) } let(:file_type) { :archive } @@ -2443,6 +2520,16 @@ RSpec.describe Ci::Build do it { is_expected.not_to be_playable } end + + context 'when build is waiting for deployment approval' do + subject { build_stubbed(:ci_build, :manual, environment: 'production') } + + before do + create(:deployment, :blocked, deployable: subject) + end + + it { is_expected.not_to be_playable } + end end describe 'project settings' do @@ -2653,6 +2740,8 @@ RSpec.describe Ci::Build do { key: 'CI_DEPENDENCY_PROXY_USER', value: 'gitlab-ci-token', public: true, masked: false }, { key: 'CI_DEPENDENCY_PROXY_PASSWORD', value: 'my-token', public: false, masked: true }, { key: 'CI_JOB_JWT', value: 'ci.job.jwt', public: false, masked: true }, + { key: 'CI_JOB_JWT_V1', value: 'ci.job.jwt', public: false, masked: true }, + { key: 'CI_JOB_JWT_V2', value: 'ci.job.jwtv2', public: false, masked: true }, { key: 'CI_JOB_NAME', value: 'test', public: true, masked: false }, { key: 'CI_JOB_STAGE', value: 'test', public: true, masked: false }, { key: 'CI_NODE_TOTAL', value: '1', public: true, masked: false }, @@ -2720,6 +2809,7 @@ RSpec.describe Ci::Build do before do allow(Gitlab::Ci::Jwt).to receive(:for_build).and_return('ci.job.jwt') + allow(Gitlab::Ci::JwtV2).to receive(:for_build).and_return('ci.job.jwtv2') build.set_token('my-token') build.yaml_variables = [] end @@ -2771,6 +2861,8 @@ RSpec.describe Ci::Build do let(:build_yaml_var) { { key: 'yaml', value: 'value', public: true, masked: false } } let(:dependency_proxy_var) { { key: 'dependency_proxy', value: 'value', public: true, masked: false } } let(:job_jwt_var) { { key: 'CI_JOB_JWT', value: 'ci.job.jwt', public: false, masked: true } } + let(:job_jwt_var_v1) { { key: 'CI_JOB_JWT_V1', value: 'ci.job.jwt', public: false, masked: true } } + let(:job_jwt_var_v2) { { key: 'CI_JOB_JWT_V2', value: 'ci.job.jwtv2', public: false, masked: true } } let(:job_dependency_var) { { key: 'job_dependency', value: 'value', public: true, masked: false } } before do @@ -2784,7 +2876,7 @@ RSpec.describe Ci::Build do allow(build).to receive(:dependency_variables) { [job_dependency_var] } allow(build).to receive(:dependency_proxy_variables) { [dependency_proxy_var] } - allow(build.project) + allow(build.pipeline.project) .to receive(:predefined_variables) { [project_pre_var] } project.variables.create!(key: 'secret', value: 'value') @@ -3084,7 +3176,7 @@ RSpec.describe Ci::Build do context 'when the branch is protected' do before do - allow(build.project).to receive(:protected_for?).with(ref).and_return(true) + allow(build.pipeline.project).to receive(:protected_for?).with(ref).and_return(true) end it { is_expected.to include(protected_variable) } @@ -3092,7 +3184,7 @@ RSpec.describe Ci::Build do context 'when the tag is protected' do before do - allow(build.project).to receive(:protected_for?).with(ref).and_return(true) + allow(build.pipeline.project).to receive(:protected_for?).with(ref).and_return(true) end it { is_expected.to include(protected_variable) } @@ -3131,7 +3223,7 @@ RSpec.describe Ci::Build do context 'when the branch is protected' do before do - allow(build.project).to receive(:protected_for?).with(ref).and_return(true) + allow(build.pipeline.project).to receive(:protected_for?).with(ref).and_return(true) end it { is_expected.to include(protected_variable) } @@ -3139,7 +3231,7 @@ RSpec.describe Ci::Build do context 'when the tag is protected' do before do - allow(build.project).to receive(:protected_for?).with(ref).and_return(true) + allow(build.pipeline.project).to receive(:protected_for?).with(ref).and_return(true) end it { is_expected.to include(protected_variable) } @@ -3526,6 +3618,20 @@ RSpec.describe Ci::Build do build.scoped_variables end + + context 'when variables builder is used' do + it 'returns the same variables' do + build.user = create(:user) + + allow(build.pipeline).to receive(:use_variables_builder_definitions?).and_return(false) + legacy_variables = build.scoped_variables.to_hash + + allow(build.pipeline).to receive(:use_variables_builder_definitions?).and_return(true) + new_variables = build.scoped_variables.to_hash + + expect(new_variables).to eq(legacy_variables) + end + end end describe '#simple_variables_without_dependencies' do @@ -3538,7 +3644,8 @@ RSpec.describe Ci::Build do shared_examples "secret CI variables" do context 'when ref is branch' do - let(:build) { create(:ci_build, ref: 'master', tag: false, project: project) } + let(:pipeline) { create(:ci_pipeline, project: project) } + let(:build) { create(:ci_build, ref: 'master', tag: false, pipeline: pipeline, project: project) } context 'when ref is protected' do before do @@ -3554,7 +3661,8 @@ RSpec.describe Ci::Build do end context 'when ref is tag' do - let(:build) { create(:ci_build, ref: 'v1.1.0', tag: true, project: project) } + let(:pipeline) { create(:ci_pipeline, project: project) } + let(:build) { create(:ci_build, ref: 'v1.1.0', tag: true, pipeline: pipeline, project: project) } context 'when ref is protected' do before do @@ -3652,8 +3760,6 @@ RSpec.describe Ci::Build do .and_return(project_variables) end - it { is_expected.to eq(project_variables) } - context 'environment is nil' do let(:environment) { nil } @@ -3661,6 +3767,35 @@ RSpec.describe Ci::Build do end end + describe '#user_variables' do + subject { build.user_variables.to_hash } + + context 'with user' do + let(:expected_variables) do + { + 'GITLAB_USER_EMAIL' => user.email, + 'GITLAB_USER_ID' => user.id.to_s, + 'GITLAB_USER_LOGIN' => user.username, + 'GITLAB_USER_NAME' => user.name + } + end + + before do + build.user = user + end + + it { is_expected.to eq(expected_variables) } + end + + context 'without user' do + before do + expect(build).to receive(:user).and_return(nil) + end + + it { is_expected.to be_empty } + end + end + describe '#any_unmet_prerequisites?' do let(:build) { create(:ci_build, :created) } @@ -3762,6 +3897,18 @@ RSpec.describe Ci::Build do end end + describe 'when the build is waiting for deployment approval' do + let(:build) { create(:ci_build, :manual, environment: 'production') } + + before do + create(:deployment, :blocked, deployable: build) + end + + it 'does not allow the build to be enqueued' do + expect { build.enqueue! }.to raise_error(StateMachines::InvalidTransition) + end + end + describe 'state transition: any => [:pending]' do let(:build) { create(:ci_build, :created) } @@ -5174,25 +5321,32 @@ RSpec.describe Ci::Build do .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 end + + context 'when build is configured to be retried' do + let(:options) { { retry: 3 } } + + context 'when there is an MR attached to the pipeline and a failed job todo for that MR' do + let!(:merge_request) { create(:merge_request, source_project: project, author: user, head_pipeline: pipeline) } + let!(:todo) { create(:todo, :build_failed, user: user, project: project, author: user, target: merge_request) } + + before do + build.update!(user: user) + project.add_developer(user) + end + + it 'resolves the todo for the old failed build' do + expect do + drop_with_exit_code + end.to change { todo.reload.state }.from('pending').to('done') + end + end + end end describe '#exit_codes_defined?' do @@ -5377,7 +5531,8 @@ RSpec.describe Ci::Build do describe '#doom!' do subject { build.doom! } - let_it_be(:build) { create(:ci_build, :queued) } + let(:traits) { [] } + let(:build) { create(:ci_build, *traits, pipeline: pipeline) } it 'updates status and failure_reason', :aggregate_failures do subject @@ -5386,10 +5541,33 @@ RSpec.describe Ci::Build do expect(build.failure_reason).to eq("data_integrity_failure") end - it 'drops associated pending build' do + it 'logs a message' do + expect(Gitlab::AppLogger) + .to receive(:info) + .with(a_hash_including(message: 'Build doomed', class: build.class.name, build_id: build.id)) + .and_call_original + subject + end + + context 'with queued builds' do + let(:traits) { [:queued] } + + it 'drops associated pending build' do + subject - expect(build.reload.queuing_entry).not_to be_present + expect(build.reload.queuing_entry).not_to be_present + end + end + + context 'with running builds' do + let(:traits) { [:picked] } + + it 'drops associated runtime metadata' do + subject + + expect(build.reload.runtime_metadata).not_to be_present + end end end diff --git a/spec/models/ci/build_trace_chunk_spec.rb b/spec/models/ci/build_trace_chunk_spec.rb index b6e128c317c..31c7c7a44bc 100644 --- a/spec/models/ci/build_trace_chunk_spec.rb +++ b/spec/models/ci/build_trace_chunk_spec.rb @@ -49,9 +49,8 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state, :clean_git end context 'FastDestroyAll' do - let(:parent) { create(:project) } - let(:pipeline) { create(:ci_pipeline, project: parent) } - let!(:build) { create(:ci_build, :running, :trace_live, pipeline: pipeline, project: parent) } + let(:pipeline) { create(:ci_pipeline) } + let!(:build) { create(:ci_build, :running, :trace_live, pipeline: pipeline) } let(:subjects) { build.trace_chunks } describe 'Forbid #destroy and #destroy_all' do @@ -84,7 +83,7 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state, :clean_git expect(external_data_counter).to be > 0 expect(subjects.count).to be > 0 - expect { parent.destroy! }.not_to raise_error + expect { pipeline.destroy! }.not_to raise_error expect(subjects.count).to eq(0) expect(external_data_counter).to eq(0) @@ -830,7 +829,7 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state, :clean_git expect(described_class.count).to eq(3) - subject + expect(subject).to be_truthy expect(described_class.count).to eq(0) @@ -852,7 +851,7 @@ RSpec.describe Ci::BuildTraceChunk, :clean_gitlab_redis_shared_state, :clean_git context 'when project is destroyed' do let(:subject) do - project.destroy! + Projects::DestroyService.new(project, project.owner).execute end it_behaves_like 'deletes all build_trace_chunk and data in redis' diff --git a/spec/models/ci/daily_build_group_report_result_spec.rb b/spec/models/ci/daily_build_group_report_result_spec.rb index acc87c61036..43ba4c32477 100644 --- a/spec/models/ci/daily_build_group_report_result_spec.rb +++ b/spec/models/ci/daily_build_group_report_result_spec.rb @@ -164,4 +164,16 @@ RSpec.describe Ci::DailyBuildGroupReportResult do end end end + + it_behaves_like 'cleanup by a loose foreign key' do + let!(:model) { create(:ci_daily_build_group_report_result) } + + let!(:parent) { model.group } + end + + it_behaves_like 'cleanup by a loose foreign key' do + let!(:model) { create(:ci_daily_build_group_report_result) } + + let!(:parent) { model.project } + end end diff --git a/spec/models/ci/freeze_period_spec.rb b/spec/models/ci/freeze_period_spec.rb index f7f840c6696..b9bf1657e28 100644 --- a/spec/models/ci/freeze_period_spec.rb +++ b/spec/models/ci/freeze_period_spec.rb @@ -5,6 +5,11 @@ require 'spec_helper' RSpec.describe Ci::FreezePeriod, type: :model do subject { build(:ci_freeze_period) } + it_behaves_like 'cleanup by a loose foreign key' do + let!(:parent) { create(:project) } + let!(:model) { create(:ci_freeze_period, project: parent) } + end + let(:invalid_cron) { '0 0 0 * *' } it { is_expected.to belong_to(:project) } diff --git a/spec/models/ci/group_variable_spec.rb b/spec/models/ci/group_variable_spec.rb index f0eec549da7..4cb3b9eef0c 100644 --- a/spec/models/ci/group_variable_spec.rb +++ b/spec/models/ci/group_variable_spec.rb @@ -42,4 +42,10 @@ RSpec.describe Ci::GroupVariable do end end end + + it_behaves_like 'cleanup by a loose foreign key' do + let!(:model) { create(:ci_group_variable) } + + let!(:parent) { model.group } + end end diff --git a/spec/models/ci/job_artifact_spec.rb b/spec/models/ci/job_artifact_spec.rb index 38061e0975f..2e8c41b410a 100644 --- a/spec/models/ci/job_artifact_spec.rb +++ b/spec/models/ci/job_artifact_spec.rb @@ -143,6 +143,17 @@ RSpec.describe Ci::JobArtifact do end end + describe '.erasable_file_types' do + subject { described_class.erasable_file_types } + + it 'returns a list of erasable file types' do + all_types = described_class.file_types.keys + erasable_types = all_types - described_class::NON_ERASABLE_FILE_TYPES + + expect(subject).to contain_exactly(*erasable_types) + end + end + describe '.erasable' do subject { described_class.erasable } @@ -534,20 +545,8 @@ RSpec.describe Ci::JobArtifact do context 'when the artifact is a trace' do let(:file_type) { :trace } - context 'when ci_store_trace_outside_transaction is enabled' do - it 'returns true' do - expect(artifact.store_after_commit?).to be_truthy - end - end - - context 'when ci_store_trace_outside_transaction is disabled' do - before do - stub_feature_flags(ci_store_trace_outside_transaction: false) - end - - it 'returns false' do - expect(artifact.store_after_commit?).to be_falsey - end + it 'returns true' do + expect(artifact.store_after_commit?).to be_truthy end end diff --git a/spec/models/ci/job_token/project_scope_link_spec.rb b/spec/models/ci/job_token/project_scope_link_spec.rb index dd6a75dfd89..8d7bb44bd16 100644 --- a/spec/models/ci/job_token/project_scope_link_spec.rb +++ b/spec/models/ci/job_token/project_scope_link_spec.rb @@ -9,6 +9,11 @@ RSpec.describe Ci::JobToken::ProjectScopeLink do let_it_be(:project) { create(:project) } + it_behaves_like 'cleanup by a loose foreign key' do + let!(:parent) { create(:user) } + let!(:model) { create(:ci_job_token_project_scope_link, added_by: parent) } + end + describe 'unique index' do let!(:link) { create(:ci_job_token_project_scope_link) } diff --git a/spec/models/ci/namespace_mirror_spec.rb b/spec/models/ci/namespace_mirror_spec.rb index b4c71f51377..a9d916115fc 100644 --- a/spec/models/ci/namespace_mirror_spec.rb +++ b/spec/models/ci/namespace_mirror_spec.rb @@ -8,50 +8,91 @@ RSpec.describe Ci::NamespaceMirror do let!(:group3) { create(:group, parent: group2) } let!(:group4) { create(:group, parent: group3) } - describe '.sync!' do - let!(:event) { namespace.sync_events.create! } + before do + # refreshing ci mirrors according to the parent tree above + Namespaces::SyncEvent.find_each { |event| Ci::NamespaceMirror.sync!(event) } + + # checking initial situation. we need to reload to reflect the changes of event sync + expect(group1.reload.ci_namespace_mirror).to have_attributes(traversal_ids: [group1.id]) + expect(group2.reload.ci_namespace_mirror).to have_attributes(traversal_ids: [group1.id, group2.id]) + expect(group3.reload.ci_namespace_mirror).to have_attributes(traversal_ids: [group1.id, group2.id, group3.id]) + expect(group4.reload.ci_namespace_mirror).to have_attributes(traversal_ids: [group1.id, group2.id, group3.id, group4.id]) + end + + context 'scopes' do + describe '.contains_namespace' do + let_it_be(:another_group) { create(:group) } + + subject(:result) { described_class.contains_namespace(group2.id) } + + it 'returns groups having group2.id in traversal_ids' do + expect(result.pluck(:namespace_id)).to contain_exactly(group2.id, group3.id, group4.id) + end + end + + describe '.contains_any_of_namespaces' do + let!(:other_group1) { create(:group) } + let!(:other_group2) { create(:group, parent: other_group1) } + let!(:other_group3) { create(:group, parent: other_group2) } + + subject(:result) { described_class.contains_any_of_namespaces([group2.id, other_group2.id]) } + + it 'returns groups having group2.id in traversal_ids' do + expect(result.pluck(:namespace_id)).to contain_exactly( + group2.id, group3.id, group4.id, other_group2.id, other_group3.id + ) + end + end + + describe '.by_namespace_id' do + subject(:result) { described_class.by_namespace_id(group2.id) } + + it 'returns namesapce mirrors of namespace id' do + expect(result).to contain_exactly(group2.ci_namespace_mirror) + end + end + end - subject(:sync) { described_class.sync!(event.reload) } + describe '.sync!' do + subject(:sync) { described_class.sync!(Namespaces::SyncEvent.last) } - context 'when namespace hierarchy does not exist in the first place' do + context 'when namespace mirror does not exist in the first place' do let(:namespace) { group3 } - it 'creates the hierarchy' do - expect { sync }.to change { described_class.count }.from(0).to(1) + before do + namespace.ci_namespace_mirror.destroy! + namespace.sync_events.create! + end + + it 'creates the mirror' do + expect { sync }.to change { described_class.count }.from(3).to(4) - expect(namespace.ci_namespace_mirror).to have_attributes(traversal_ids: [group1.id, group2.id, group3.id]) + expect(namespace.reload.ci_namespace_mirror).to have_attributes(traversal_ids: [group1.id, group2.id, group3.id]) end end - context 'when namespace hierarchy does already exist' do + context 'when namespace mirror does already exist' do let(:namespace) { group3 } before do - described_class.create!(namespace: namespace, traversal_ids: [namespace.id]) + namespace.sync_events.create! end - it 'updates the hierarchy' do + it 'updates the mirror' do expect { sync }.not_to change { described_class.count } - expect(namespace.ci_namespace_mirror).to have_attributes(traversal_ids: [group1.id, group2.id, group3.id]) + expect(namespace.reload.ci_namespace_mirror).to have_attributes(traversal_ids: [group1.id, group2.id, group3.id]) end end - # I did not extract this context to a `shared_context` because the behavior will change - # after implementing the TODO in `Ci::NamespaceMirror.sync!` - context 'changing the middle namespace' do + shared_context 'changing the middle namespace' do let(:namespace) { group2 } before do - described_class.create!(namespace_id: group1.id, traversal_ids: [group1.id]) - described_class.create!(namespace_id: group2.id, traversal_ids: [group1.id, group2.id]) - described_class.create!(namespace_id: group3.id, traversal_ids: [group1.id, group2.id, group3.id]) - described_class.create!(namespace_id: group4.id, traversal_ids: [group1.id, group2.id, group3.id, group4.id]) - - group2.update!(parent: nil) + group2.update!(parent: nil) # creates a sync event end - it 'updates hierarchies for the base but wait for events for the children' do + it 'updates traversal_ids for the base and descendants' do expect { sync }.not_to change { described_class.count } expect(group1.reload.ci_namespace_mirror).to have_attributes(traversal_ids: [group1.id]) @@ -61,6 +102,8 @@ RSpec.describe Ci::NamespaceMirror do end end + it_behaves_like 'changing the middle namespace' + context 'when the FFs sync_traversal_ids, use_traversal_ids and use_traversal_ids_for_ancestors are disabled' do before do stub_feature_flags(sync_traversal_ids: false, @@ -68,27 +111,7 @@ RSpec.describe Ci::NamespaceMirror do use_traversal_ids_for_ancestors: false) end - context 'changing the middle namespace' do - let(:namespace) { group2 } - - before do - described_class.create!(namespace_id: group1.id, traversal_ids: [group1.id]) - described_class.create!(namespace_id: group2.id, traversal_ids: [group1.id, group2.id]) - described_class.create!(namespace_id: group3.id, traversal_ids: [group1.id, group2.id, group3.id]) - described_class.create!(namespace_id: group4.id, traversal_ids: [group1.id, group2.id, group3.id, group4.id]) - - group2.update!(parent: nil) - end - - it 'updates hierarchies for the base and descendants' do - expect { sync }.not_to change { described_class.count } - - expect(group1.reload.ci_namespace_mirror).to have_attributes(traversal_ids: [group1.id]) - expect(group2.reload.ci_namespace_mirror).to have_attributes(traversal_ids: [group2.id]) - expect(group3.reload.ci_namespace_mirror).to have_attributes(traversal_ids: [group2.id, group3.id]) - expect(group4.reload.ci_namespace_mirror).to have_attributes(traversal_ids: [group2.id, group3.id, group4.id]) - end - end + it_behaves_like 'changing the middle namespace' end end end diff --git a/spec/models/ci/pending_build_spec.rb b/spec/models/ci/pending_build_spec.rb index abf0fb443bb..5692444339f 100644 --- a/spec/models/ci/pending_build_spec.rb +++ b/spec/models/ci/pending_build_spec.rb @@ -223,4 +223,14 @@ RSpec.describe Ci::PendingBuild do end end end + + it_behaves_like 'cleanup by a loose foreign key' do + let!(:parent) { create(:namespace) } + let!(:model) { create(:ci_pending_build, namespace: parent) } + end + + it_behaves_like 'cleanup by a loose foreign key' do + let!(:parent) { create(:project) } + let!(:model) { create(:ci_pending_build, project: parent) } + end end diff --git a/spec/models/ci/pipeline_artifact_spec.rb b/spec/models/ci/pipeline_artifact_spec.rb index f65483d2290..801505f0231 100644 --- a/spec/models/ci/pipeline_artifact_spec.rb +++ b/spec/models/ci/pipeline_artifact_spec.rb @@ -215,4 +215,11 @@ RSpec.describe Ci::PipelineArtifact, type: :model do end end end + + context 'loose foreign key on ci_pipeline_artifacts.project_id' do + it_behaves_like 'cleanup by a loose foreign key' do + let!(:parent) { create(:project) } + let!(:model) { create(:ci_pipeline_artifact, project: parent) } + end + end end diff --git a/spec/models/ci/pipeline_schedule_spec.rb b/spec/models/ci/pipeline_schedule_spec.rb index fee74f8f674..0f1cb721e95 100644 --- a/spec/models/ci/pipeline_schedule_spec.rb +++ b/spec/models/ci/pipeline_schedule_spec.rb @@ -23,6 +23,11 @@ RSpec.describe Ci::PipelineSchedule do subject { build(:ci_pipeline_schedule, project: project) } end + it_behaves_like 'cleanup by a loose foreign key' do + let!(:parent) { create(:user) } + let!(:model) { create(:ci_pipeline_schedule, owner: parent) } + end + describe 'validations' do it 'does not allow invalid cron patterns' do pipeline_schedule = build(:ci_pipeline_schedule, cron: '0 0 0 * *') diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index fd9970699d7..90f56c1e0a4 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -31,6 +31,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do it { is_expected.to have_many(:statuses_order_id_desc) } it { is_expected.to have_many(:bridges) } it { is_expected.to have_many(:job_artifacts).through(:builds) } + it { is_expected.to have_many(:build_trace_chunks).through(:builds) } it { is_expected.to have_many(:auto_canceled_pipelines) } it { is_expected.to have_many(:auto_canceled_jobs) } it { is_expected.to have_many(:sourced_pipelines) } @@ -1516,30 +1517,12 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do end describe 'pipeline caching' do - context 'when expire_job_and_pipeline_cache_synchronously is enabled' do - before do - stub_feature_flags(expire_job_and_pipeline_cache_synchronously: true) - end - - it 'executes Ci::ExpirePipelineCacheService' do - expect_next_instance_of(Ci::ExpirePipelineCacheService) do |service| - expect(service).to receive(:execute).with(pipeline) - end - - pipeline.cancel + it 'executes Ci::ExpirePipelineCacheService' do + expect_next_instance_of(Ci::ExpirePipelineCacheService) do |service| + expect(service).to receive(:execute).with(pipeline) end - end - - context 'when expire_job_and_pipeline_cache_synchronously is disabled' do - before do - stub_feature_flags(expire_job_and_pipeline_cache_synchronously: false) - end - - it 'performs ExpirePipelinesCacheWorker' do - expect(ExpirePipelineCacheWorker).to receive(:perform_async).with(pipeline.id) - pipeline.cancel - end + pipeline.cancel end end @@ -4677,4 +4660,23 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do let!(:model) { create(:ci_pipeline, user: create(:user)) } let!(:parent) { model.user } end + + describe 'tags count' do + let_it_be_with_refind(:pipeline) do + create(:ci_empty_pipeline, project: project) + end + + it { expect(pipeline.tags_count).to eq(0) } + it { expect(pipeline.distinct_tags_count).to eq(0) } + + context 'with builds' do + before do + create(:ci_build, pipeline: pipeline, tag_list: %w[a b]) + create(:ci_build, pipeline: pipeline, tag_list: %w[b c]) + end + + it { expect(pipeline.tags_count).to eq(4) } + it { expect(pipeline.distinct_tags_count).to eq(3) } + end + end end diff --git a/spec/models/ci/project_mirror_spec.rb b/spec/models/ci/project_mirror_spec.rb index 199285b036c..5ef520b4230 100644 --- a/spec/models/ci/project_mirror_spec.rb +++ b/spec/models/ci/project_mirror_spec.rb @@ -8,12 +8,36 @@ RSpec.describe Ci::ProjectMirror do let!(:project) { create(:project, namespace: group2) } + context 'scopes' do + let_it_be(:another_project) { create(:project, namespace: group1) } + + describe '.by_project_id' do + subject(:result) { described_class.by_project_id(project.id) } + + it 'returns project mirrors of project' do + expect(result.pluck(:project_id)).to contain_exactly(project.id) + end + end + + describe '.by_namespace_id' do + subject(:result) { described_class.by_namespace_id(group2.id) } + + it 'returns project mirrors of namespace id' do + expect(result).to contain_exactly(project.ci_project_mirror) + end + end + end + describe '.sync!' do let!(:event) { Projects::SyncEvent.create!(project: project) } - subject(:sync) { described_class.sync!(event.reload) } + subject(:sync) { described_class.sync!(event) } + + context 'when project mirror does not exist in the first place' do + before do + project.ci_project_mirror.destroy! + end - context 'when project hierarchy does not exist in the first place' do it 'creates a ci_projects record' do expect { sync }.to change { described_class.count }.from(0).to(1) @@ -21,11 +45,7 @@ RSpec.describe Ci::ProjectMirror do end end - context 'when project hierarchy does already exist' do - before do - described_class.create!(project_id: project.id, namespace_id: group1.id) - end - + context 'when project mirror does already exist' do it 'updates the related ci_projects record' do expect { sync }.not_to change { described_class.count } diff --git a/spec/models/ci/resource_group_spec.rb b/spec/models/ci/resource_group_spec.rb index aae16157fbf..76e74f3193c 100644 --- a/spec/models/ci/resource_group_spec.rb +++ b/spec/models/ci/resource_group_spec.rb @@ -3,6 +3,11 @@ require 'spec_helper' RSpec.describe Ci::ResourceGroup do + it_behaves_like 'cleanup by a loose foreign key' do + let!(:parent) { create(:project) } + let!(:model) { create(:ci_resource_group, project: parent) } + end + describe 'validation' do it 'valids when key includes allowed character' do resource_group = build(:ci_resource_group, key: 'test') diff --git a/spec/models/ci/runner_namespace_spec.rb b/spec/models/ci/runner_namespace_spec.rb index 41d805adb9f..2d1fe11147c 100644 --- a/spec/models/ci/runner_namespace_spec.rb +++ b/spec/models/ci/runner_namespace_spec.rb @@ -6,4 +6,10 @@ RSpec.describe Ci::RunnerNamespace do it_behaves_like 'includes Limitable concern' do subject { build(:ci_runner_namespace, group: create(:group, :nested), runner: create(:ci_runner, :group)) } end + + it_behaves_like 'cleanup by a loose foreign key' do + let!(:model) { create(:ci_runner_namespace) } + + let!(:parent) { model.namespace } + end end diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb index 5142f70fa2c..6830a8daa3b 100644 --- a/spec/models/ci/runner_spec.rb +++ b/spec/models/ci/runner_spec.rb @@ -48,7 +48,7 @@ RSpec.describe Ci::Runner do let(:runner) { create(:ci_runner, :group, groups: [group]) } it 'disallows assigning group if already assigned to a group' do - runner.runner_namespaces << build(:ci_runner_namespace) + runner.runner_namespaces << create(:ci_runner_namespace) expect(runner).not_to be_valid expect(runner.errors.full_messages).to include('Runner needs to be assigned to exactly one group') @@ -203,28 +203,56 @@ RSpec.describe Ci::Runner do end end - describe '.belonging_to_parent_group_of_project' do - let(:project) { create(:project, group: group) } - let(:group) { create(:group) } - let(:runner) { create(:ci_runner, :group, groups: [group]) } - let!(:unrelated_group) { create(:group) } - let!(:unrelated_project) { create(:project, group: unrelated_group) } - let!(:unrelated_runner) { create(:ci_runner, :group, groups: [unrelated_group]) } + shared_examples '.belonging_to_parent_group_of_project' do + let!(:group1) { create(:group) } + let!(:project1) { create(:project, group: group1) } + let!(:runner1) { create(:ci_runner, :group, groups: [group1]) } + + let!(:group2) { create(:group) } + let!(:project2) { create(:project, group: group2) } + let!(:runner2) { create(:ci_runner, :group, groups: [group2]) } + + let(:project_id) { project1.id } + + subject(:result) { described_class.belonging_to_parent_group_of_project(project_id) } it 'returns the specific group runner' do - expect(described_class.belonging_to_parent_group_of_project(project.id)).to contain_exactly(runner) + expect(result).to contain_exactly(runner1) end - context 'with a parent group with a runner' do - let(:runner) { create(:ci_runner, :group, groups: [parent_group]) } - let(:project) { create(:project, group: group) } - let(:group) { create(:group, parent: parent_group) } - let(:parent_group) { create(:group) } + context 'with a parent group with a runner', :sidekiq_inline do + before do + group1.update!(parent: group2) + end - it 'returns the group runner from the parent group' do - expect(described_class.belonging_to_parent_group_of_project(project.id)).to contain_exactly(runner) + it 'returns the group runner from the group and the parent group' do + expect(result).to contain_exactly(runner1, runner2) end end + + context 'with multiple project ids' do + let(:project_id) { [project1.id, project2.id] } + + it 'raises ArgumentError' do + expect { result }.to raise_error(ArgumentError) + end + end + end + + context 'when use_traversal_ids* are enabled' do + it_behaves_like '.belonging_to_parent_group_of_project' + end + + context 'when use_traversal_ids* are disabled' do + before do + stub_feature_flags( + use_traversal_ids: false, + use_traversal_ids_for_ancestors: false, + use_traversal_ids_for_ancestor_scopes: false + ) + end + + it_behaves_like '.belonging_to_parent_group_of_project' end describe '.owned_or_instance_wide' do @@ -1358,7 +1386,7 @@ RSpec.describe Ci::Runner do it { is_expected.to eq(contacted_at_stored) } end - describe '.belonging_to_group' do + describe '.legacy_belonging_to_group' do shared_examples 'returns group runners' do it 'returns the specific group runner' do group = create(:group) @@ -1366,7 +1394,7 @@ RSpec.describe Ci::Runner do unrelated_group = create(:group) create(:ci_runner, :group, groups: [unrelated_group]) - expect(described_class.belonging_to_group(group.id)).to contain_exactly(runner) + expect(described_class.legacy_belonging_to_group(group.id)).to contain_exactly(runner) end context 'runner belonging to parent group' do @@ -1376,13 +1404,13 @@ RSpec.describe Ci::Runner do context 'when include_parent option is passed' do it 'returns the group runner from the parent group' do - expect(described_class.belonging_to_group(group.id, include_ancestors: true)).to contain_exactly(parent_runner) + expect(described_class.legacy_belonging_to_group(group.id, include_ancestors: true)).to contain_exactly(parent_runner) end end context 'when include_parent option is not passed' do it 'does not return the group runner from the parent group' do - expect(described_class.belonging_to_group(group.id)).to be_empty + expect(described_class.legacy_belonging_to_group(group.id)).to be_empty end end end @@ -1398,4 +1426,48 @@ RSpec.describe Ci::Runner do it_behaves_like 'returns group runners' end end + + describe '.belonging_to_group' do + it 'returns the specific group runner' do + group = create(:group) + runner = create(:ci_runner, :group, groups: [group]) + unrelated_group = create(:group) + create(:ci_runner, :group, groups: [unrelated_group]) + + expect(described_class.belonging_to_group(group.id)).to contain_exactly(runner) + end + end + + describe '.belonging_to_group_and_ancestors' do + let_it_be(:parent_group) { create(:group) } + let_it_be(:parent_runner) { create(:ci_runner, :group, groups: [parent_group]) } + let_it_be(:group) { create(:group, parent: parent_group) } + + it 'returns the group runner from the parent group' do + expect(described_class.belonging_to_group_and_ancestors(group.id)).to contain_exactly(parent_runner) + end + end + + describe '.belonging_to_group_or_project_descendants' do + it 'returns the specific group runners' do + group1 = create(:group) + group2 = create(:group, parent: group1) + group3 = create(:group) + + project1 = create(:project, namespace: group1) + project2 = create(:project, namespace: group2) + project3 = create(:project, namespace: group3) + + runner1 = create(:ci_runner, :group, groups: [group1]) + runner2 = create(:ci_runner, :group, groups: [group2]) + _runner3 = create(:ci_runner, :group, groups: [group3]) + runner4 = create(:ci_runner, :project, projects: [project1]) + runner5 = create(:ci_runner, :project, projects: [project2]) + _runner6 = create(:ci_runner, :project, projects: [project3]) + + expect(described_class.belonging_to_group_or_project_descendants(group1.id)).to contain_exactly( + runner1, runner2, runner4, runner5 + ) + end + end end diff --git a/spec/models/ci/running_build_spec.rb b/spec/models/ci/running_build_spec.rb index 629861e35b8..d2f74494308 100644 --- a/spec/models/ci/running_build_spec.rb +++ b/spec/models/ci/running_build_spec.rb @@ -49,4 +49,9 @@ RSpec.describe Ci::RunningBuild do end end end + + it_behaves_like 'cleanup by a loose foreign key' do + let!(:parent) { create(:project) } + let!(:model) { create(:ci_running_build, project: parent) } + end end diff --git a/spec/models/ci/secure_file_spec.rb b/spec/models/ci/secure_file_spec.rb new file mode 100644 index 00000000000..ae57b63e7a4 --- /dev/null +++ b/spec/models/ci/secure_file_spec.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Ci::SecureFile do + let(:sample_file) { fixture_file('ci_secure_files/upload-keystore.jks') } + + subject { create(:ci_secure_file) } + + before do + stub_ci_secure_file_object_storage + end + + it { is_expected.to be_a FileStoreMounter } + + it { is_expected.to belong_to(:project).required } + + it_behaves_like 'having unique enum values' + + describe 'validations' do + it { is_expected.to validate_presence_of(:checksum) } + it { is_expected.to validate_presence_of(:file_store) } + it { is_expected.to validate_presence_of(:name) } + it { is_expected.to validate_presence_of(:permissions) } + it { is_expected.to validate_presence_of(:project_id) } + end + + describe '#permissions' do + it 'defaults to read_only file permssions' do + expect(subject.permissions).to eq('read_only') + end + end + + describe '#checksum' do + it 'computes SHA256 checksum on the file before encrypted' do + subject.file = CarrierWaveStringFile.new(sample_file) + subject.save! + expect(subject.checksum).to eq(Digest::SHA256.hexdigest(sample_file)) + end + end + + describe '#checksum_algorithm' do + it 'returns the configured checksum_algorithm' do + expect(subject.checksum_algorithm).to eq('sha256') + end + end + + describe '#file' do + it 'returns the saved file' do + subject.file = CarrierWaveStringFile.new(sample_file) + subject.save! + expect(Base64.encode64(subject.file.read)).to eq(Base64.encode64(sample_file)) + end + end +end diff --git a/spec/models/ci/unit_test_spec.rb b/spec/models/ci/unit_test_spec.rb index 2207a362be3..556cf93c266 100644 --- a/spec/models/ci/unit_test_spec.rb +++ b/spec/models/ci/unit_test_spec.rb @@ -3,6 +3,11 @@ require 'spec_helper' RSpec.describe Ci::UnitTest do + it_behaves_like 'cleanup by a loose foreign key' do + let!(:parent) { create(:project) } + let!(:model) { create(:ci_unit_test, project: parent) } + end + describe 'relationships' do it { is_expected.to belong_to(:project) } it { is_expected.to have_many(:unit_test_failures) } |