diff options
Diffstat (limited to 'spec/models/ci')
-rw-r--r-- | spec/models/ci/bridge_spec.rb | 220 | ||||
-rw-r--r-- | spec/models/ci/build_need_spec.rb | 18 | ||||
-rw-r--r-- | spec/models/ci/build_spec.rb | 36 | ||||
-rw-r--r-- | spec/models/ci/catalog/components_project_spec.rb | 1 | ||||
-rw-r--r-- | spec/models/ci/catalog/listing_spec.rb | 292 | ||||
-rw-r--r-- | spec/models/ci/catalog/resource_spec.rb | 270 | ||||
-rw-r--r-- | spec/models/ci/catalog/resources/sync_event_spec.rb | 190 | ||||
-rw-r--r-- | spec/models/ci/catalog/resources/version_spec.rb | 50 | ||||
-rw-r--r-- | spec/models/ci/job_artifact_spec.rb | 10 | ||||
-rw-r--r-- | spec/models/ci/job_token/scope_spec.rb | 12 | ||||
-rw-r--r-- | spec/models/ci/pipeline_metadata_spec.rb | 18 | ||||
-rw-r--r-- | spec/models/ci/pipeline_spec.rb | 126 | ||||
-rw-r--r-- | spec/models/ci/processable_spec.rb | 9 | ||||
-rw-r--r-- | spec/models/ci/runner_manager_build_spec.rb | 2 | ||||
-rw-r--r-- | spec/models/ci/runner_manager_spec.rb | 2 | ||||
-rw-r--r-- | spec/models/ci/runner_version_spec.rb | 2 |
16 files changed, 982 insertions, 276 deletions
diff --git a/spec/models/ci/bridge_spec.rb b/spec/models/ci/bridge_spec.rb index 1d0c3bb5dee..ae8c5aea858 100644 --- a/spec/models/ci/bridge_spec.rb +++ b/spec/models/ci/bridge_spec.rb @@ -36,6 +36,24 @@ RSpec.describe Ci::Bridge, feature_category: :continuous_integration do expect(bridge).to have_one(:downstream_pipeline) end + describe 'no-op methods for compatibility with Ci::Build' do + it 'returns an empty array job_artifacts' do + expect(bridge.job_artifacts).to eq(Ci::JobArtifact.none) + end + + it 'return nil for artifacts_expire_at' do + expect(bridge.artifacts_expire_at).to be_nil + end + + it 'return nil for runner' do + expect(bridge.runner).to be_nil + end + + it 'returns an empty TagList for tag_list' do + expect(bridge.tag_list).to be_a(ActsAsTaggableOn::TagList) + end + end + describe '#retryable?' do let(:bridge) { create(:ci_bridge, :success) } @@ -595,6 +613,203 @@ RSpec.describe Ci::Bridge, feature_category: :continuous_integration do end end + describe 'variables expansion' do + let(:options) do + { + trigger: { + project: 'my/project', + branch: 'master', + forward: { yaml_variables: true, + pipeline_variables: true }.compact + } + } + end + + let(:yaml_variables) do + [ + { + key: 'EXPANDED_PROJECT_VAR6', + value: 'project value6 $PROJECT_PROTECTED_VAR' + }, + { + key: 'EXPANDED_GROUP_VAR6', + value: 'group value6 $GROUP_PROTECTED_VAR' + }, + + { + key: 'VAR7', + value: 'value7 $VAR1', + raw: true + } + ] + end + + let_it_be(:downstream_creator_user) { create(:user) } + let_it_be(:bridge_creator_user) { create(:user) } + + let_it_be(:bridge_group) { create(:group) } + let_it_be(:downstream_group) { create(:group) } + let_it_be(:downstream_project) { create(:project, creator: downstream_creator_user, group: downstream_group) } + let_it_be(:project) { create(:project, :repository, :in_group, creator: bridge_creator_user, group: bridge_group) } + let(:bridge) { build(:ci_bridge, :playable, pipeline: pipeline, downstream: downstream_project) } + let!(:pipeline) { create(:ci_pipeline, project: project) } + + let!(:ci_variable) do + create(:ci_variable, + project: project, + key: 'PROJECT_PROTECTED_VAR', + value: 'this is a secret', + protected: is_variable_protected?) + end + + let!(:ci_group_variable) do + create(:ci_group_variable, + group: bridge_group, + key: 'GROUP_PROTECTED_VAR', + value: 'this is a secret', + protected: is_variable_protected?) + end + + before do + bridge.yaml_variables = yaml_variables + allow(bridge.project).to receive(:protected_for?).and_return(true) + end + + shared_examples 'expands variables from a project downstream' do + it do + vars = bridge.downstream_variables + expect(vars).to include({ key: 'EXPANDED_PROJECT_VAR6', value: 'project value6 this is a secret' }) + end + end + + shared_examples 'expands variables from a group downstream' do + it do + vars = bridge.downstream_variables + expect(vars).to include({ key: 'EXPANDED_GROUP_VAR6', value: 'group value6 this is a secret' }) + end + end + + shared_examples 'expands project and group variables downstream' do + it_behaves_like 'expands variables from a project downstream' + + it_behaves_like 'expands variables from a group downstream' + end + + shared_examples 'does not expand variables from a project downstream' do + it do + vars = bridge.downstream_variables + expect(vars).not_to include({ key: 'EXPANDED_PROJECT_VAR6', value: 'project value6 this is a secret' }) + end + end + + shared_examples 'does not expand variables from a group downstream' do + it do + vars = bridge.downstream_variables + expect(vars).not_to include({ key: 'EXPANDED_GROUP_VAR6', value: 'group value6 this is a secret' }) + end + end + + shared_examples 'feature flag is disabled' do + before do + stub_feature_flags(exclude_protected_variables_from_multi_project_pipeline_triggers: false) + end + + it_behaves_like 'expands project and group variables downstream' + end + + shared_examples 'does not expand project and group variables downstream' do + it_behaves_like 'does not expand variables from a project downstream' + + it_behaves_like 'does not expand variables from a group downstream' + end + + context 'when they are protected' do + let!(:is_variable_protected?) { true } + + context 'and downstream project group is different from bridge group' do + it_behaves_like 'does not expand project and group variables downstream' + + it_behaves_like 'feature flag is disabled' + end + + context 'and there is no downstream project' do + let(:downstream_project) { nil } + + it_behaves_like 'expands project and group variables downstream' + + it_behaves_like 'feature flag is disabled' + end + + context 'and downstream project equals bridge project' do + let(:downstream_project) { project } + + it_behaves_like 'expands project and group variables downstream' + + it_behaves_like 'feature flag is disabled' + end + + context 'and downstream project group is equal to bridge project group' do + let_it_be(:downstream_project) { create(:project, creator: downstream_creator_user, group: bridge_group) } + + it_behaves_like 'expands variables from a group downstream' + + it_behaves_like 'does not expand variables from a project downstream' + + it_behaves_like 'feature flag is disabled' + end + + context 'and downstream project has no group' do + let_it_be(:downstream_project) { create(:project, creator: downstream_creator_user) } + + it_behaves_like 'does not expand project and group variables downstream' + + it_behaves_like 'feature flag is disabled' + end + end + + context 'when they are not protected' do + let!(:is_variable_protected?) { false } + + context 'and downstream project group is different from bridge group' do + it_behaves_like 'expands project and group variables downstream' + + it_behaves_like 'feature flag is disabled' + end + + context 'and there is no downstream project' do + let(:downstream_project) { nil } + + it_behaves_like 'expands project and group variables downstream' + + it_behaves_like 'feature flag is disabled' + end + + context 'and downstream project equals bridge project' do + let(:downstream_project) { project } + + it_behaves_like 'expands project and group variables downstream' + + it_behaves_like 'feature flag is disabled' + end + + context 'and downstream project group is equal to bridge project group' do + let_it_be(:downstream_project) { create(:project, creator: downstream_creator_user, group: bridge_group) } + + it_behaves_like 'expands project and group variables downstream' + + it_behaves_like 'feature flag is disabled' + end + + context 'and downstream project has no group' do + let_it_be(:downstream_project) { create(:project, creator: downstream_creator_user) } + + it_behaves_like 'expands project and group variables downstream' + + it_behaves_like 'feature flag is disabled' + end + end + end + describe '#forward_pipeline_variables?' do using RSpec::Parameterized::TableSyntax @@ -824,8 +1039,9 @@ RSpec.describe Ci::Bridge, feature_category: :continuous_integration do end it 'creates the metadata record and assigns its partition' do - # the factory doesn't use any metadatable setters by default - # so the record will be initialized by the before_validation callback + # The record is initialized by the factory calling metadatable setters + bridge.metadata = nil + expect(bridge.metadata).to be_nil expect(bridge.save!).to be_truthy diff --git a/spec/models/ci/build_need_spec.rb b/spec/models/ci/build_need_spec.rb index 4f76a7650ec..7ce3c63458f 100644 --- a/spec/models/ci/build_need_spec.rb +++ b/spec/models/ci/build_need_spec.rb @@ -11,11 +11,21 @@ RSpec.describe Ci::BuildNeed, model: true, feature_category: :continuous_integra it { is_expected.to validate_presence_of(:name) } it { is_expected.to validate_length_of(:name).is_at_most(255) } - describe '.artifacts' do - let_it_be(:with_artifacts) { create(:ci_build_need, artifacts: true) } - let_it_be(:without_artifacts) { create(:ci_build_need, artifacts: false) } + describe 'scopes' do + describe '.scoped_build' do + subject(:scoped_build) { described_class.scoped_build } - it { expect(described_class.artifacts).to contain_exactly(with_artifacts) } + it 'includes partition_id filter' do + expect(scoped_build.where_values_hash).to match(a_hash_including('partition_id')) + end + end + + describe '.artifacts' do + let_it_be(:with_artifacts) { create(:ci_build_need, artifacts: true) } + let_it_be(:without_artifacts) { create(:ci_build_need, artifacts: false) } + + it { expect(described_class.artifacts).to contain_exactly(with_artifacts) } + end end describe 'BulkInsertSafe' do diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 2e552c8d524..18c7e57d464 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -987,6 +987,28 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def describe '#artifacts_public?' do subject { build.artifacts_public? } + context 'artifacts with defaults - public' do + let(:build) { create(:ci_build, :artifacts, pipeline: pipeline) } + + it { is_expected.to be_truthy } + end + + context 'non public artifacts' do + let(:build) { create(:ci_build, :private_artifacts, pipeline: pipeline) } + + it { is_expected.to be_falsey } + end + + context 'no artifacts' do + let(:build) { create(:ci_build, pipeline: pipeline) } + + it { is_expected.to be_truthy } + end + end + + describe '#artifact_is_public_in_config?' do + subject { build.artifact_is_public_in_config? } + context 'artifacts with defaults' do let(:build) { create(:ci_build, :artifacts, pipeline: pipeline) } @@ -994,10 +1016,22 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def end context 'non public artifacts' do - let(:build) { create(:ci_build, :artifacts, :with_private_artifacts_config, pipeline: pipeline) } + let(:build) { create(:ci_build, :with_private_artifacts_config, pipeline: pipeline) } it { is_expected.to be_falsey } end + + context 'public artifacts' do + let(:build) { create(:ci_build, :with_public_artifacts_config, pipeline: pipeline) } + + it { is_expected.to be_truthy } + end + + context 'no artifacts' do + let(:build) { create(:ci_build, pipeline: pipeline) } + + it { is_expected.to be_truthy } + end end describe '#artifacts_expired?' do diff --git a/spec/models/ci/catalog/components_project_spec.rb b/spec/models/ci/catalog/components_project_spec.rb index 79e1a113e47..5f739c244a5 100644 --- a/spec/models/ci/catalog/components_project_spec.rb +++ b/spec/models/ci/catalog/components_project_spec.rb @@ -97,6 +97,7 @@ RSpec.describe Ci::Catalog::ComponentsProject, feature_category: :pipeline_compo 'dast' | 'image: alpine_2' | 'templates/dast/template.yml' 'template' | 'image: alpine_3' | 'templates/template.yml' 'blank-yaml' | '' | 'templates/blank-yaml.yml' + 'non/exist' | nil | nil end with_them do diff --git a/spec/models/ci/catalog/listing_spec.rb b/spec/models/ci/catalog/listing_spec.rb index 7a1e12165ac..2ffffb9112c 100644 --- a/spec/models/ci/catalog/listing_spec.rb +++ b/spec/models/ci/catalog/listing_spec.rb @@ -3,59 +3,61 @@ require 'spec_helper' RSpec.describe Ci::Catalog::Listing, feature_category: :pipeline_composition do - let_it_be(:namespace) { create(:group) } - let_it_be(:project_x) { create(:project, namespace: namespace, name: 'X Project') } - let_it_be(:project_a) { create(:project, :public, namespace: namespace, name: 'A Project') } - let_it_be(:project_noaccess) { create(:project, namespace: namespace, name: 'C Project') } - let_it_be(:project_ext) { create(:project, :public, name: 'TestProject') } let_it_be(:user) { create(:user) } + let_it_be(:namespace) { create(:group) } + let_it_be(:public_namespace_project) do + create(:project, :public, namespace: namespace, name: 'A public namespace project') + end - let_it_be(:project_b) do + let_it_be(:public_project) { create(:project, :public, name: 'B public test project') } + let_it_be(:namespace_project_a) { create(:project, namespace: namespace, name: 'Test namespace project') } + let_it_be(:namespace_project_b) { create(:project, namespace: namespace, name: 'X namespace Project') } + let_it_be(:project_noaccess) { create(:project, namespace: namespace, name: 'Project with no access') } + let_it_be(:internal_project) { create(:project, :internal, name: 'Internal project') } + + let_it_be(:private_project) do create(:project, namespace: namespace, name: 'B Project', description: 'Rspec test framework') end let(:list) { described_class.new(user) } before_all do - project_x.add_reporter(user) - project_b.add_reporter(user) - project_a.add_reporter(user) - project_ext.add_reporter(user) + namespace_project_a.add_reporter(user) + namespace_project_b.add_reporter(user) + public_namespace_project.add_reporter(user) + public_project.add_reporter(user) + internal_project.add_owner(user) end describe '#resources' do subject(:resources) { list.resources(**params) } - context 'when user is anonymous' do - let(:user) { nil } - let(:params) { {} } + let(:params) { {} } - let!(:resource_1) { create(:ci_catalog_resource, project: project_a) } - let!(:resource_2) { create(:ci_catalog_resource, project: project_ext) } - let!(:resource_3) { create(:ci_catalog_resource, project: project_b) } + let_it_be(:public_resource_a) { create(:ci_catalog_resource, :published, project: public_namespace_project) } + let_it_be(:public_resource_b) { create(:ci_catalog_resource, :published, project: public_project) } + let_it_be(:internal_resource) { create(:ci_catalog_resource, :published, project: internal_project) } + let_it_be(:private_namespace_resource) { create(:ci_catalog_resource, :published, project: namespace_project_a) } + let_it_be(:unpublished_resource) { create(:ci_catalog_resource, project: namespace_project_b) } - it 'returns only resources for public projects' do - is_expected.to contain_exactly(resource_1, resource_2) - end + it 'by default returns all resources visible to the current user' do + is_expected.to contain_exactly(public_resource_a, public_resource_b, private_namespace_resource, + internal_resource) + end - context 'when sorting is provided' do - let(:params) { { sort: :name_desc } } + context 'when user is anonymous' do + let(:user) { nil } - it 'returns only resources for public projects sorted by name DESC' do - is_expected.to contain_exactly(resource_2, resource_1) - end + it 'returns only published resources for public projects' do + is_expected.to contain_exactly(public_resource_a, public_resource_b) end end context 'when search params are provided' do let(:params) { { search: 'test' } } - let!(:resource_1) { create(:ci_catalog_resource, project: project_a) } - let!(:resource_2) { create(:ci_catalog_resource, project: project_ext) } - let!(:resource_3) { create(:ci_catalog_resource, project: project_b) } - it 'returns the resources that match the search params' do - is_expected.to contain_exactly(resource_2, resource_3) + is_expected.to contain_exactly(public_resource_b, private_namespace_resource) end context 'when search term is too small' do @@ -65,117 +67,197 @@ RSpec.describe Ci::Catalog::Listing, feature_category: :pipeline_composition do end end - context 'when namespace is provided' do - let(:params) { { namespace: namespace } } + context 'when the scope is :namespaces' do + let_it_be(:public_resource_no_namespace) do + create(:ci_catalog_resource, project: create(:project, :public, name: 'public')) + end - context 'when namespace is not a root namespace' do - let(:namespace) { create(:group, :nested) } + let(:params) { { scope: :namespaces } } - it 'raises an exception' do - expect { resources }.to raise_error(ArgumentError, 'Namespace is not a root namespace') + context 'when the `ci_guard_query_for_catalog_resource_scope` ff is enabled' do + it "returns the catalog resources belonging to the user's authorized namespaces" do + is_expected.to contain_exactly(public_resource_a, public_resource_b, internal_resource, + private_namespace_resource) end end - context 'when the user has access to all projects in the namespace' do - context 'when the namespace has no catalog resources' do - it { is_expected.to be_empty } + context 'when the `ci_guard_query_for_catalog_resource_scope` ff is disabled' do + before do + stub_feature_flags(ci_guard_for_catalog_resource_scope: false) end - context 'when the namespace has catalog resources' do - let_it_be(:today) { Time.zone.now } - let_it_be(:yesterday) { today - 1.day } - let_it_be(:tomorrow) { today + 1.day } + it 'returns all resources visible to the current user' do + is_expected.to contain_exactly( + public_resource_a, public_resource_b, private_namespace_resource, + internal_resource) + end + end + end - let_it_be(:resource_1) do - create(:ci_catalog_resource, project: project_x, latest_released_at: yesterday, created_at: today) - end + context 'with a sort parameter' do + let_it_be(:today) { Time.zone.now } + let_it_be(:yesterday) { today - 1.day } + let_it_be(:tomorrow) { today + 1.day } - let_it_be(:resource_2) do - create(:ci_catalog_resource, project: project_b, latest_released_at: today, created_at: yesterday) - end + let(:params) { { sort: sort } } - let_it_be(:resource_3) do - create(:ci_catalog_resource, project: project_a, latest_released_at: nil, created_at: tomorrow) - end + before_all do + public_resource_a.update!(created_at: today, latest_released_at: yesterday) + public_resource_b.update!(created_at: yesterday, latest_released_at: today) + private_namespace_resource.update!(created_at: tomorrow, latest_released_at: tomorrow) + internal_resource.update!(created_at: tomorrow + 1) + end - let_it_be(:other_namespace_resource) do - create(:ci_catalog_resource, project: project_ext, latest_released_at: tomorrow) - end + context 'when the sort is created_at ascending' do + let_it_be(:sort) { :created_at_asc } + + it 'contains catalog resources sorted by created_at ascending' do + is_expected.to eq([public_resource_b, public_resource_a, private_namespace_resource, internal_resource]) + end + end + + context 'when the sort is created_at descending' do + let_it_be(:sort) { :created_at_desc } + + it 'contains catalog resources sorted by created_at descending' do + is_expected.to eq([internal_resource, private_namespace_resource, public_resource_a, public_resource_b]) + end + end + + context 'when the sort is name ascending' do + let_it_be(:sort) { :name_asc } + + it 'contains catalog resources for projects sorted by name ascending' do + is_expected.to eq([public_resource_a, public_resource_b, internal_resource, private_namespace_resource]) + end + end + + context 'when the sort is name descending' do + let_it_be(:sort) { :name_desc } + + it 'contains catalog resources for projects sorted by name descending' do + is_expected.to eq([private_namespace_resource, internal_resource, public_resource_b, public_resource_a]) + end + end - it 'contains only catalog resources for projects in that namespace' do - is_expected.to contain_exactly(resource_1, resource_2, resource_3) + context 'when the sort is latest_released_at ascending' do + let_it_be(:sort) { :latest_released_at_asc } + + it 'contains catalog resources sorted by latest_released_at ascending with nulls last' do + is_expected.to eq([public_resource_a, public_resource_b, private_namespace_resource, internal_resource]) + end + end + + context 'when the sort is latest_released_at descending' do + let_it_be(:sort) { :latest_released_at_desc } + + it 'contains catalog resources sorted by latest_released_at descending with nulls last' do + is_expected.to eq([private_namespace_resource, public_resource_b, public_resource_a, internal_resource]) + end + end + end + + context 'when namespace is provided' do + let(:params) { { namespace: namespace } } + + context 'when it is a root namespace' do + context 'when it has catalog resources' do + it 'returns resources in the namespace visible to the user' do + is_expected.to contain_exactly(public_resource_a, private_namespace_resource) end + end - context 'with a sort parameter' do - let(:params) { { namespace: namespace, sort: sort } } + context 'when the namespace has no catalog resources' do + let(:namespace) { build(:namespace) } - context 'when the sort is created_at ascending' do - let_it_be(:sort) { :created_at_asc } + it { is_expected.to be_empty } + end + end - it 'contains catalog resources sorted by created_at ascending' do - is_expected.to eq([resource_2, resource_1, resource_3]) - end - end + context 'when namespace is not a root namespace' do + let_it_be(:namespace) { create(:group, :nested) } - context 'when the sort is created_at descending' do - let_it_be(:sort) { :created_at_desc } + it 'raises an exception' do + expect { resources }.to raise_error(ArgumentError, 'Namespace is not a root namespace') + end + end + end + end - it 'contains catalog resources sorted by created_at descending' do - is_expected.to eq([resource_3, resource_1, resource_2]) - end - end + describe '#find_resource' do + let_it_be(:accessible_resource) { create(:ci_catalog_resource, :published, project: public_project) } + let_it_be(:inaccessible_resource) { create(:ci_catalog_resource, :published, project: project_noaccess) } + let_it_be(:draft_resource) { create(:ci_catalog_resource, project: public_namespace_project, state: :draft) } - context 'when the sort is name ascending' do - let_it_be(:sort) { :name_asc } + context 'when using the ID argument' do + subject { list.find_resource(id: id) } - it 'contains catalog resources for projects sorted by name ascending' do - is_expected.to eq([resource_3, resource_2, resource_1]) - end - end + context 'when the resource is published and visible to the user' do + let(:id) { accessible_resource.id } - context 'when the sort is name descending' do - let_it_be(:sort) { :name_desc } + it 'fetches the resource' do + is_expected.to eq(accessible_resource) + end + end - it 'contains catalog resources for projects sorted by name descending' do - is_expected.to eq([resource_1, resource_2, resource_3]) - end - end + context 'when the resource is not found' do + let(:id) { 'not-an-id' } - context 'when the sort is latest_released_at ascending' do - let_it_be(:sort) { :latest_released_at_asc } + it 'returns nil' do + is_expected.to be_nil + end + end - it 'contains catalog resources sorted by latest_released_at ascending with nulls last' do - is_expected.to eq([resource_1, resource_2, resource_3]) - end - end + context 'when the resource is not published' do + let(:id) { draft_resource.id } - context 'when the sort is latest_released_at descending' do - let_it_be(:sort) { :latest_released_at_desc } + it 'returns nil' do + is_expected.to be_nil + end + end - it 'contains catalog resources sorted by latest_released_at descending with nulls last' do - is_expected.to eq([resource_2, resource_1, resource_3]) - end - end - end + context "when the current user cannot read code on the resource's project" do + let(:id) { inaccessible_resource.id } + + it 'returns nil' do + is_expected.to be_nil end end + end - context 'when the user only has access to some projects in the namespace' do - let!(:accessible_resource) { create(:ci_catalog_resource, project: project_x) } - let!(:inaccessible_resource) { create(:ci_catalog_resource, project: project_noaccess) } + context 'when using the full_path argument' do + subject { list.find_resource(full_path: full_path) } - it 'only returns catalog resources for projects the user has access to' do - is_expected.to contain_exactly(accessible_resource) + context 'when the resource is published and visible to the user' do + let(:full_path) { accessible_resource.project.full_path } + + it 'fetches the resource' do + is_expected.to eq(accessible_resource) end end - context 'when the user does not have access to the namespace' do - let!(:project) { create(:project) } - let!(:resource) { create(:ci_catalog_resource, project: project) } + context 'when the resource is not found' do + let(:full_path) { 'not-a-path' } - let(:namespace) { project.namespace } + it 'returns nil' do + is_expected.to be_nil + end + end - it { is_expected.to be_empty } + context 'when the resource is not published' do + let(:full_path) { draft_resource.project.full_path } + + it 'returns nil' do + is_expected.to be_nil + end + end + + context "when the current user cannot read code on the resource's project" do + let(:full_path) { inaccessible_resource.project.full_path } + + it 'returns nil' do + is_expected.to be_nil + end end end end diff --git a/spec/models/ci/catalog/resource_spec.rb b/spec/models/ci/catalog/resource_spec.rb index 098772b1ea9..15d8b4f440b 100644 --- a/spec/models/ci/catalog/resource_spec.rb +++ b/spec/models/ci/catalog/resource_spec.rb @@ -3,50 +3,57 @@ require 'spec_helper' RSpec.describe Ci::Catalog::Resource, feature_category: :pipeline_composition do - let_it_be(:today) { Time.zone.now } - let_it_be(:yesterday) { today - 1.day } - let_it_be(:tomorrow) { today + 1.day } + let_it_be(:current_user) { create(:user) } - let_it_be_with_reload(:project) { create(:project, name: 'A') } - let_it_be(:project_2) { build(:project, name: 'Z') } - let_it_be(:project_3) { build(:project, name: 'L', description: 'Z') } - let_it_be_with_reload(:resource) { create(:ci_catalog_resource, project: project, latest_released_at: tomorrow) } - let_it_be(:resource_2) { create(:ci_catalog_resource, project: project_2, latest_released_at: today) } - let_it_be(:resource_3) { create(:ci_catalog_resource, project: project_3, latest_released_at: nil) } + let_it_be(:project_a) { create(:project, name: 'A') } + let_it_be(:project_b) { create(:project, name: 'B') } + let_it_be(:project_c) { create(:project, name: 'C', description: 'B') } - let_it_be(:release1) { create(:release, project: project, released_at: yesterday) } - let_it_be(:release2) { create(:release, project: project, released_at: today) } - let_it_be(:release3) { create(:release, project: project, released_at: tomorrow) } + let_it_be_with_reload(:resource_a) do + create(:ci_catalog_resource, project: project_a, latest_released_at: '2023-02-01T00:00:00Z') + end + + let_it_be(:resource_b) do + create(:ci_catalog_resource, project: project_b, latest_released_at: '2023-01-01T00:00:00Z') + end + + let_it_be(:resource_c) { create(:ci_catalog_resource, project: project_c) } it { is_expected.to belong_to(:project) } it do is_expected.to( - have_many(:components).class_name('Ci::Catalog::Resources::Component').with_foreign_key(:catalog_resource_id) - ) + have_many(:components).class_name('Ci::Catalog::Resources::Component').with_foreign_key(:catalog_resource_id)) end - it { is_expected.to have_many(:versions).class_name('Ci::Catalog::Resources::Version') } + it do + is_expected.to( + have_many(:versions).class_name('Ci::Catalog::Resources::Version').with_foreign_key(:catalog_resource_id)) + end + + it do + is_expected.to( + have_many(:sync_events).class_name('Ci::Catalog::Resources::SyncEvent').with_foreign_key(:catalog_resource_id)) + end it { is_expected.to delegate_method(:avatar_path).to(:project) } it { is_expected.to delegate_method(:star_count).to(:project) } - it { is_expected.to delegate_method(:forks_count).to(:project) } it { is_expected.to define_enum_for(:state).with_values({ draft: 0, published: 1 }) } describe '.for_projects' do it 'returns catalog resources for the given project IDs' do - resources_for_projects = described_class.for_projects(project.id) + resources_for_projects = described_class.for_projects(project_a.id) - expect(resources_for_projects).to contain_exactly(resource) + expect(resources_for_projects).to contain_exactly(resource_a) end end describe '.search' do it 'returns catalog resources whose name or description match the search term' do - resources = described_class.search('Z') + resources = described_class.search('B') - expect(resources).to contain_exactly(resource_2, resource_3) + expect(resources).to contain_exactly(resource_b, resource_c) end end @@ -54,7 +61,7 @@ RSpec.describe Ci::Catalog::Resource, feature_category: :pipeline_composition do it 'returns catalog resources sorted by descending created at' do ordered_resources = described_class.order_by_created_at_desc - expect(ordered_resources.to_a).to eq([resource_3, resource_2, resource]) + expect(ordered_resources.to_a).to eq([resource_c, resource_b, resource_a]) end end @@ -62,7 +69,7 @@ RSpec.describe Ci::Catalog::Resource, feature_category: :pipeline_composition do it 'returns catalog resources sorted by ascending created at' do ordered_resources = described_class.order_by_created_at_asc - expect(ordered_resources.to_a).to eq([resource, resource_2, resource_3]) + expect(ordered_resources.to_a).to eq([resource_a, resource_b, resource_c]) end end @@ -70,13 +77,13 @@ RSpec.describe Ci::Catalog::Resource, feature_category: :pipeline_composition do subject(:ordered_resources) { described_class.order_by_name_desc } it 'returns catalog resources sorted by descending name' do - expect(ordered_resources.pluck(:name)).to eq(%w[Z L A]) + expect(ordered_resources.pluck(:name)).to eq(%w[C B A]) end it 'returns catalog resources sorted by descending name with nulls last' do - resource.update!(name: nil) + resource_a.update!(name: nil) - expect(ordered_resources.pluck(:name)).to eq(['Z', 'L', nil]) + expect(ordered_resources.pluck(:name)).to eq(['C', 'B', nil]) end end @@ -84,13 +91,13 @@ RSpec.describe Ci::Catalog::Resource, feature_category: :pipeline_composition do subject(:ordered_resources) { described_class.order_by_name_asc } it 'returns catalog resources sorted by ascending name' do - expect(ordered_resources.pluck(:name)).to eq(%w[A L Z]) + expect(ordered_resources.pluck(:name)).to eq(%w[A B C]) end it 'returns catalog resources sorted by ascending name with nulls last' do - resource.update!(name: nil) + resource_a.update!(name: nil) - expect(ordered_resources.pluck(:name)).to eq(['L', 'Z', nil]) + expect(ordered_resources.pluck(:name)).to eq(['B', 'C', nil]) end end @@ -98,7 +105,7 @@ RSpec.describe Ci::Catalog::Resource, feature_category: :pipeline_composition do it 'returns catalog resources sorted by latest_released_at descending with nulls last' do ordered_resources = described_class.order_by_latest_released_at_desc - expect(ordered_resources).to eq([resource, resource_2, resource_3]) + expect(ordered_resources).to eq([resource_a, resource_b, resource_c]) end end @@ -106,96 +113,215 @@ RSpec.describe Ci::Catalog::Resource, feature_category: :pipeline_composition do it 'returns catalog resources sorted by latest_released_at ascending with nulls last' do ordered_resources = described_class.order_by_latest_released_at_asc - expect(ordered_resources).to eq([resource_2, resource, resource_3]) + expect(ordered_resources).to eq([resource_b, resource_a, resource_c]) + end + end + + describe 'authorized catalog resources' do + let_it_be(:namespace) { create(:group) } + let_it_be(:other_namespace) { create(:group) } + let_it_be(:other_user) { create(:user) } + + let_it_be(:public_project) { create(:project, :public) } + let_it_be(:internal_project) { create(:project, :internal) } + let_it_be(:internal_namespace_project) { create(:project, :internal, namespace: namespace) } + let_it_be(:private_namespace_project) { create(:project, namespace: namespace) } + let_it_be(:other_private_namespace_project) { create(:project, namespace: other_namespace) } + + let_it_be(:public_resource) { create(:ci_catalog_resource, project: public_project) } + let_it_be(:internal_resource) { create(:ci_catalog_resource, project: internal_project) } + let_it_be(:internal_namespace_resource) { create(:ci_catalog_resource, project: internal_namespace_project) } + let_it_be(:private_namespace_resource) { create(:ci_catalog_resource, project: private_namespace_project) } + + let_it_be(:other_private_namespace_resource) do + create(:ci_catalog_resource, project: other_private_namespace_project) + end + + before_all do + namespace.add_reporter(current_user) + other_namespace.add_guest(other_user) + end + + describe '.public_or_visible_to_user' do + subject(:resources) { described_class.public_or_visible_to_user(current_user) } + + it 'returns all resources visible to the user' do + expect(resources).to contain_exactly( + public_resource, internal_resource, internal_namespace_resource, private_namespace_resource) + end + + context 'with a different user' do + let(:current_user) { other_user } + + it 'returns all resources visible to the user' do + expect(resources).to contain_exactly( + public_resource, internal_resource, internal_namespace_resource, other_private_namespace_resource) + end + end + + context 'when the user is nil' do + let(:current_user) { nil } + + it 'returns only public resources' do + expect(resources).to contain_exactly(public_resource) + end + end + end + + describe '.visible_to_user' do + subject(:resources) { described_class.visible_to_user(current_user) } + + it "returns resources belonging to the user's authorized namespaces" do + expect(resources).to contain_exactly(internal_namespace_resource, private_namespace_resource) + end + + context 'with a different user' do + let(:current_user) { other_user } + + it "returns resources belonging to the user's authorized namespaces" do + expect(resources).to contain_exactly(other_private_namespace_resource) + end + end + + context 'when the user is nil' do + let(:current_user) { nil } + + it 'does not return any resources' do + expect(resources).to be_empty + end + end end end describe '#state' do it 'defaults to draft' do - expect(resource.state).to eq('draft') + expect(resource_a.state).to eq('draft') end end describe '#publish!' do context 'when the catalog resource is in draft state' do it 'updates the state of the catalog resource to published' do - expect(resource.state).to eq('draft') + expect(resource_a.state).to eq('draft') - resource.publish! + resource_a.publish! - expect(resource.reload.state).to eq('published') + expect(resource_a.reload.state).to eq('published') end end - context 'when a catalog resource already has a published state' do + context 'when the catalog resource already has a published state' do it 'leaves the state as published' do - resource.update!(state: 'published') + resource_a.update!(state: :published) + expect(resource_a.state).to eq('published') - resource.publish! + resource_a.publish! - expect(resource.state).to eq('published') + expect(resource_a.state).to eq('published') end end end - describe '#unpublish!' do - context 'when the catalog resource is in published state' do - it 'updates the state to draft' do - resource.update!(state: :published) - expect(resource.state).to eq('published') + describe 'synchronizing denormalized columns with `projects` table', :sidekiq_inline do + let_it_be_with_reload(:project) { create(:project, name: 'Test project', description: 'Test description') } - resource.unpublish! + context 'when the catalog resource is created' do + let(:resource) { build(:ci_catalog_resource, project: project) } + + it 'updates the catalog resource columns to match the project' do + resource.save! + resource.reload - expect(resource.reload.state).to eq('draft') + expect(resource.name).to eq(project.name) + expect(resource.description).to eq(project.description) + expect(resource.visibility_level).to eq(project.visibility_level) end end - context 'when the catalog resource is already in draft state' do - it 'leaves the state as draft' do - expect(resource.state).to eq('draft') + context 'when the project is updated' do + let_it_be(:resource) { create(:ci_catalog_resource, project: project) } + + context 'when project name is updated' do + it 'updates the catalog resource name to match' do + project.update!(name: 'New name') + + expect(resource.reload.name).to eq(project.name) + end + end + + context 'when project description is updated' do + it 'updates the catalog resource description to match' do + project.update!(description: 'New description') + + expect(resource.reload.description).to eq(project.description) + end + end - resource.unpublish! + context 'when project visibility_level is updated' do + it 'updates the catalog resource visibility_level to match' do + project.update!(visibility_level: Gitlab::VisibilityLevel::INTERNAL) - expect(resource.reload.state).to eq('draft') + expect(resource.reload.visibility_level).to eq(project.visibility_level) + end end end end - describe 'sync with project' do - shared_examples 'denormalized columns of the catalog resource match the project' do - it do - expect(resource.name).to eq(project.name) - expect(resource.description).to eq(project.description) - expect(resource.visibility_level).to eq(project.visibility_level) - end + describe '#update_latest_released_at! triggered in model callbacks' do + let_it_be(:project) { create(:project) } + let_it_be(:resource) { create(:ci_catalog_resource, project: project) } + + let_it_be_with_refind(:january_release) do + create(:release, :with_catalog_resource_version, project: project, tag: 'v1', released_at: '2023-01-01T00:00:00Z') end - context 'when the catalog resource is created' do - it_behaves_like 'denormalized columns of the catalog resource match the project' + let_it_be_with_refind(:february_release) do + create(:release, :with_catalog_resource_version, project: project, tag: 'v2', released_at: '2023-02-01T00:00:00Z') end - context 'when the project name is updated' do - before do - project.update!(name: 'My new project name') - end + it 'has the expected latest_released_at value' do + expect(resource.reload.latest_released_at).to eq(february_release.released_at) + end + + context 'when a new catalog resource version is created' do + it 'updates the latest_released_at value' do + march_release = create(:release, :with_catalog_resource_version, project: project, tag: 'v3', + released_at: '2023-03-01T00:00:00Z') - it_behaves_like 'denormalized columns of the catalog resource match the project' + expect(resource.reload.latest_released_at).to eq(march_release.released_at) + end end - context 'when the project description is updated' do - before do - project.update!(description: 'My new description') + context 'when a catalog resource version is destroyed' do + it 'updates the latest_released_at value' do + february_release.catalog_resource_version.destroy! + + expect(resource.reload.latest_released_at).to eq(january_release.released_at) end + end + + context 'when the released_at value of a release is updated' do + it 'updates the latest_released_at value' do + january_release.update!(released_at: '2024-01-01T00:00:00Z') - it_behaves_like 'denormalized columns of the catalog resource match the project' + expect(resource.reload.latest_released_at).to eq(january_release.released_at) + end end - context 'when the project visibility_level is updated' do - before do - project.update!(visibility_level: 10) + context 'when a release is destroyed' do + it 'updates the latest_released_at value' do + february_release.destroy! + expect(resource.reload.latest_released_at).to eq(january_release.released_at) end + end - it_behaves_like 'denormalized columns of the catalog resource match the project' + context 'when all releases associated with the catalog resource are destroyed' do + it 'updates the latest_released_at value to nil' do + january_release.destroy! + february_release.destroy! + + expect(resource.reload.latest_released_at).to be_nil + end end end end diff --git a/spec/models/ci/catalog/resources/sync_event_spec.rb b/spec/models/ci/catalog/resources/sync_event_spec.rb new file mode 100644 index 00000000000..5d907aae9b6 --- /dev/null +++ b/spec/models/ci/catalog/resources/sync_event_spec.rb @@ -0,0 +1,190 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Ci::Catalog::Resources::SyncEvent, type: :model, feature_category: :pipeline_composition do + let_it_be_with_reload(:project1) { create(:project) } + let_it_be_with_reload(:project2) { create(:project) } + let_it_be(:resource1) { create(:ci_catalog_resource, project: project1) } + + it { is_expected.to belong_to(:catalog_resource).class_name('Ci::Catalog::Resource') } + it { is_expected.to belong_to(:project) } + + describe 'PG triggers' do + context 'when the associated project of a catalog resource is updated' do + context 'when project name is updated' do + it 'creates a sync event record' do + expect do + project1.update!(name: 'New name') + end.to change { described_class.count }.by(1) + end + end + + context 'when project description is updated' do + it 'creates a sync event record' do + expect do + project1.update!(description: 'New description') + end.to change { described_class.count }.by(1) + end + end + + context 'when project visibility_level is updated' do + it 'creates a sync event record' do + expect do + project1.update!(visibility_level: Gitlab::VisibilityLevel::INTERNAL) + end.to change { described_class.count }.by(1) + end + end + end + + context 'when a project without an associated catalog resource is updated' do + it 'does not create a sync event record' do + expect do + project2.update!(name: 'New name') + end.not_to change { described_class.count } + end + end + end + + describe 'when there are sync event records' do + let_it_be(:resource2) { create(:ci_catalog_resource, project: project2) } + + before_all do + create(:ci_catalog_resource_sync_event, catalog_resource: resource1, status: :processed) + create(:ci_catalog_resource_sync_event, catalog_resource: resource1) + create_list(:ci_catalog_resource_sync_event, 2, catalog_resource: resource2) + end + + describe '.unprocessed_events' do + it 'returns the events in pending status' do + # 1 pending event from resource1 + 2 pending events from resource2 + expect(described_class.unprocessed_events.size).to eq(3) + end + + it 'selects the partition attribute in the result' do + described_class.unprocessed_events.each do |event| + expect(event.partition).not_to be_nil + end + end + end + + describe '.mark_records_processed' do + it 'updates the records to processed status' do + expect(described_class.status_pending.count).to eq(3) + expect(described_class.status_processed.count).to eq(1) + + described_class.mark_records_processed(described_class.unprocessed_events) + + expect(described_class.pluck(:status).uniq).to eq(['processed']) + + expect(described_class.status_pending.count).to eq(0) + expect(described_class.status_processed.count).to eq(4) + end + end + end + + describe '.upper_bound_count' do + it 'returns 0 when there are no records in the table' do + expect(described_class.upper_bound_count).to eq(0) + end + + it 'returns an estimated number of unprocessed records' do + create_list(:ci_catalog_resource_sync_event, 5, catalog_resource: resource1) + described_class.order(:id).limit(2).update_all(status: :processed) + + expect(described_class.upper_bound_count).to eq(3) + end + end + + describe 'sliding_list partitioning' do + let(:partition_manager) { Gitlab::Database::Partitioning::PartitionManager.new(described_class) } + + describe 'next_partition_if callback' do + let(:active_partition) { described_class.partitioning_strategy.active_partition } + + subject(:value) { described_class.partitioning_strategy.next_partition_if.call(active_partition) } + + context 'when the partition is empty' do + it { is_expected.to eq(false) } + end + + context 'when the partition has records' do + before do + create(:ci_catalog_resource_sync_event, catalog_resource: resource1, status: :processed) + create(:ci_catalog_resource_sync_event, catalog_resource: resource1) + end + + it { is_expected.to eq(false) } + end + + context 'when the first record of the partition is older than PARTITION_DURATION' do + before do + create(:ci_catalog_resource_sync_event, catalog_resource: resource1) + described_class.first.update!(created_at: (described_class::PARTITION_DURATION + 1.day).ago) + end + + it { is_expected.to eq(true) } + end + end + + describe 'detach_partition_if callback' do + let(:active_partition) { described_class.partitioning_strategy.active_partition } + + subject(:value) { described_class.partitioning_strategy.detach_partition_if.call(active_partition) } + + before_all do + create(:ci_catalog_resource_sync_event, catalog_resource: resource1, status: :processed) + create(:ci_catalog_resource_sync_event, catalog_resource: resource1) + end + + context 'when the partition contains unprocessed records' do + it { is_expected.to eq(false) } + end + + context 'when the partition contains only processed records' do + before do + described_class.update_all(status: :processed) + end + + it { is_expected.to eq(true) } + end + end + + describe 'strategy behavior' do + it 'moves records to new partitions as time passes', :freeze_time do + # We start with partition 1 + expect(described_class.partitioning_strategy.current_partitions.map(&:value)).to eq([1]) + + # Add one record so the initial partition is not empty + create(:ci_catalog_resource_sync_event, catalog_resource: resource1) + + # It's not a day old yet so no new partitions are created + partition_manager.sync_partitions + + expect(described_class.partitioning_strategy.current_partitions.map(&:value)).to eq([1]) + + # After traveling forward a day + travel(described_class::PARTITION_DURATION + 1.second) + + # a new partition is created + partition_manager.sync_partitions + + expect(described_class.partitioning_strategy.current_partitions.map(&:value)).to contain_exactly(1, 2) + + # and we can insert to the new partition + create(:ci_catalog_resource_sync_event, catalog_resource: resource1) + + # After processing records in partition 1 + described_class.mark_records_processed(described_class.for_partition(1).select_with_partition) + + partition_manager.sync_partitions + + # partition 1 is removed + expect(described_class.partitioning_strategy.current_partitions.map(&:value)).to eq([2]) + + # and we only have the newly created partition left. + expect(described_class.count).to eq(1) + end + end + end +end diff --git a/spec/models/ci/catalog/resources/version_spec.rb b/spec/models/ci/catalog/resources/version_spec.rb index 7114d2b6709..aafd51699b5 100644 --- a/spec/models/ci/catalog/resources/version_spec.rb +++ b/spec/models/ci/catalog/resources/version_spec.rb @@ -10,9 +10,6 @@ RSpec.describe Ci::Catalog::Resources::Version, type: :model, feature_category: it { is_expected.to belong_to(:project) } it { is_expected.to have_many(:components).class_name('Ci::Catalog::Resources::Component') } - it { is_expected.to delegate_method(:name).to(:release) } - it { is_expected.to delegate_method(:description).to(:release) } - it { is_expected.to delegate_method(:tag).to(:release) } it { is_expected.to delegate_method(:sha).to(:release) } it { is_expected.to delegate_method(:released_at).to(:release) } it { is_expected.to delegate_method(:author_id).to(:release) } @@ -104,4 +101,51 @@ RSpec.describe Ci::Catalog::Resources::Version, type: :model, feature_category: end end end + + describe '#update_catalog_resource' do + let_it_be(:release) { create(:release, project: project1, tag: 'v1') } + let(:version) { build(:ci_catalog_resource_version, catalog_resource: resource1, release: release) } + + context 'when a version is created' do + it 'calls update_catalog_resource' do + expect(version).to receive(:update_catalog_resource).once + + version.save! + end + end + + context 'when a version is destroyed' do + it 'calls update_catalog_resource' do + version.save! + + expect(version).to receive(:update_catalog_resource).once + + version.destroy! + end + end + end + + describe '#name' do + it 'is equivalent to release.tag' do + release_v1_0.update!(name: 'Release v1.0') + + expect(v1_0.name).to eq(release_v1_0.tag) + end + end + + describe '#commit' do + subject(:commit) { v1_0.commit } + + it 'returns a commit' do + is_expected.to be_a(Commit) + end + + context 'when the sha is nil' do + it 'returns nil' do + release_v1_0.update!(sha: nil) + + is_expected.to be_nil + end + end + end end diff --git a/spec/models/ci/job_artifact_spec.rb b/spec/models/ci/job_artifact_spec.rb index 48d46824c11..e65c1e2f577 100644 --- a/spec/models/ci/job_artifact_spec.rb +++ b/spec/models/ci/job_artifact_spec.rb @@ -176,16 +176,6 @@ RSpec.describe Ci::JobArtifact, feature_category: :build_artifacts do let!(:artifact) { build(:ci_job_artifact, :private) } it { is_expected.to be_falsey } - - context 'and the non_public_artifacts feature flag is disabled' do - let!(:artifact) { build(:ci_job_artifact, :private) } - - before do - stub_feature_flags(non_public_artifacts: false) - end - - it { is_expected.to be_truthy } - end end end diff --git a/spec/models/ci/job_token/scope_spec.rb b/spec/models/ci/job_token/scope_spec.rb index d41286f5a45..adb9f461f63 100644 --- a/spec/models/ci/job_token/scope_spec.rb +++ b/spec/models/ci/job_token/scope_spec.rb @@ -160,18 +160,6 @@ RSpec.describe Ci::JobToken::Scope, feature_category: :continuous_integration, f with_them do it { is_expected.to eq(result) } end - - context "with FF restrict_ci_job_token_for_public_and_internal_projects disabled" do - before do - stub_feature_flags(restrict_ci_job_token_for_public_and_internal_projects: false) - end - - let(:accessed_project) { unscoped_public_project } - - it "restricts public and internal outbound projects not in allowlist" do - is_expected.to eq(false) - end - end end end end diff --git a/spec/models/ci/pipeline_metadata_spec.rb b/spec/models/ci/pipeline_metadata_spec.rb index 977c90bcc2a..1a426118063 100644 --- a/spec/models/ci/pipeline_metadata_spec.rb +++ b/spec/models/ci/pipeline_metadata_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Ci::PipelineMetadata do +RSpec.describe Ci::PipelineMetadata, feature_category: :pipeline_composition do it { is_expected.to belong_to(:project) } it { is_expected.to belong_to(:pipeline) } @@ -10,5 +10,21 @@ RSpec.describe Ci::PipelineMetadata do it { is_expected.to validate_length_of(:name).is_at_least(1).is_at_most(255) } it { is_expected.to validate_presence_of(:project) } it { is_expected.to validate_presence_of(:pipeline) } + + it do + is_expected.to define_enum_for( + :auto_cancel_on_new_commit + ).with_values( + conservative: 0, interruptible: 1, disabled: 2 + ).with_prefix + end + + it do + is_expected.to define_enum_for( + :auto_cancel_on_job_failure + ).with_values( + none: 0, all: 1 + ).with_prefix + end end end diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 9696ba7b3ee..024d3ae4240 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -86,6 +86,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep, feature_category: it { is_expected.to respond_to :short_sha } it { is_expected.to delegate_method(:full_path).to(:project).with_prefix } it { is_expected.to delegate_method(:name).to(:pipeline_metadata).allow_nil } + it { is_expected.to delegate_method(:auto_cancel_on_job_failure).to(:pipeline_metadata).allow_nil } describe 'validations' do it { is_expected.to validate_presence_of(:sha) } @@ -163,7 +164,6 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep, feature_category: before do stub_const('Ci::Refs::UnlockPreviousPipelinesWorker', unlock_previous_pipelines_worker_spy) - stub_feature_flags(ci_stop_unlock_pipelines: false) end shared_examples 'not unlocking pipelines' do |event:| @@ -202,42 +202,6 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep, feature_category: it_behaves_like 'unlocking pipelines', event: :skip it_behaves_like 'unlocking pipelines', event: :cancel it_behaves_like 'unlocking pipelines', event: :block - - context 'and ci_stop_unlock_pipelines is enabled' do - before do - stub_feature_flags(ci_stop_unlock_pipelines: true) - end - - it_behaves_like 'not unlocking pipelines', event: :succeed - it_behaves_like 'not unlocking pipelines', event: :drop - it_behaves_like 'not unlocking pipelines', event: :skip - it_behaves_like 'not unlocking pipelines', event: :cancel - it_behaves_like 'not unlocking pipelines', event: :block - end - - context 'and ci_unlock_non_successful_pipelines is disabled' do - before do - stub_feature_flags(ci_unlock_non_successful_pipelines: false) - end - - it_behaves_like 'unlocking pipelines', event: :succeed - it_behaves_like 'not unlocking pipelines', event: :drop - it_behaves_like 'not unlocking pipelines', event: :skip - it_behaves_like 'not unlocking pipelines', event: :cancel - it_behaves_like 'not unlocking pipelines', event: :block - - context 'and ci_stop_unlock_pipelines is enabled' do - before do - stub_feature_flags(ci_stop_unlock_pipelines: true) - end - - it_behaves_like 'not unlocking pipelines', event: :succeed - it_behaves_like 'not unlocking pipelines', event: :drop - it_behaves_like 'not unlocking pipelines', event: :skip - it_behaves_like 'not unlocking pipelines', event: :cancel - it_behaves_like 'not unlocking pipelines', event: :block - end - end end context 'when transitioning to a non-unlockable state' do @@ -246,14 +210,6 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep, feature_category: end it_behaves_like 'not unlocking pipelines', event: :run - - context 'and ci_unlock_non_successful_pipelines is disabled' do - before do - stub_feature_flags(ci_unlock_non_successful_pipelines: false) - end - - it_behaves_like 'not unlocking pipelines', event: :run - end end end @@ -2028,17 +1984,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep, feature_category: end end - context 'when only_allow_merge_if_pipeline_succeeds? returns false and widget_pipeline_pass_subscription_update disabled' do - let(:only_allow_merge_if_pipeline_succeeds?) { false } - - before do - stub_feature_flags(widget_pipeline_pass_subscription_update: false) - end - - it_behaves_like 'state transition not triggering GraphQL subscription mergeRequestMergeStatusUpdated' - end - - context 'when only_allow_merge_if_pipeline_succeeds? returns false and widget_pipeline_pass_subscription_update enabled' do + context 'when only_allow_merge_if_pipeline_succeeds? returns false' do let(:only_allow_merge_if_pipeline_succeeds?) { false } it_behaves_like 'triggers GraphQL subscription mergeRequestMergeStatusUpdated' do @@ -3848,6 +3794,44 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep, feature_category: end end + describe '#set_failed' do + let(:pipeline) { build(:ci_pipeline) } + + it 'marks the pipeline as failed with the given reason without saving', :aggregate_failures do + pipeline.set_failed(:filtered_by_rules) + + expect(pipeline).to be_failed + expect(pipeline).to be_filtered_by_rules + expect(pipeline).not_to be_persisted + end + end + + describe '#filtered_as_empty?' do + let(:pipeline) { build_stubbed(:ci_pipeline) } + + subject { pipeline.filtered_as_empty? } + + it { is_expected.to eq false } + + context 'when the pipeline is failed' do + using RSpec::Parameterized::TableSyntax + + where(:drop_reason, :expected) do + :unknown_failure | false + :filtered_by_rules | true + :filtered_by_workflow_rules | true + end + + with_them do + before do + pipeline.set_failed(drop_reason) + end + + it { is_expected.to eq expected } + end + end + end + describe '#has_yaml_errors?' do let(:pipeline) { build_stubbed(:ci_pipeline) } @@ -4065,8 +4049,8 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep, feature_category: describe '#builds_in_self_and_project_descendants' do subject(:builds) { pipeline.builds_in_self_and_project_descendants } - let(:pipeline) { create(:ci_pipeline) } - let!(:build) { create(:ci_build, pipeline: pipeline) } + let_it_be_with_refind(:pipeline) { create(:ci_pipeline) } + let_it_be(:build) { create(:ci_build, pipeline: pipeline) } context 'when pipeline is standalone' do it 'returns the list of builds' do @@ -4093,6 +4077,10 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep, feature_category: expect(builds).to contain_exactly(build, child_build, child_of_child_build) end end + + it 'includes partition_id filter' do + expect(builds.where_values_hash).to match(a_hash_including('partition_id' => pipeline.partition_id)) + end end describe '#build_with_artifacts_in_self_and_project_descendants' do @@ -4118,7 +4106,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep, feature_category: describe '#jobs_in_self_and_project_descendants' do subject(:jobs) { pipeline.jobs_in_self_and_project_descendants } - let(:pipeline) { create(:ci_pipeline) } + let_it_be_with_refind(:pipeline) { create(:ci_pipeline) } shared_examples_for 'fetches jobs in self and project descendant pipelines' do |factory_type| let!(:job) { create(factory_type, pipeline: pipeline) } @@ -4151,6 +4139,10 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep, feature_category: expect(jobs).to contain_exactly(job, child_job, child_of_child_job, child_source_bridge, child_of_child_source_bridge) end end + + it 'includes partition_id filter' do + expect(jobs.where_values_hash).to match(a_hash_including('partition_id' => pipeline.partition_id)) + end end context 'when job is build' do @@ -5651,6 +5643,22 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep, feature_category: end end + describe '.current_partition_value' do + subject { described_class.current_partition_value } + + it { is_expected.to eq(101) } + + it 'accepts an optional argument' do + expect(described_class.current_partition_value(build_stubbed(:project))).to eq(101) + end + + it 'returns 100 when the flag is disabled' do + stub_feature_flags(ci_current_partition_value_101: false) + + is_expected.to eq(100) + end + end + describe '#notes=' do context 'when notes already exist' do it 'does not create duplicate notes', :aggregate_failures do diff --git a/spec/models/ci/processable_spec.rb b/spec/models/ci/processable_spec.rb index 8c0143d5f18..5d457c4f213 100644 --- a/spec/models/ci/processable_spec.rb +++ b/spec/models/ci/processable_spec.rb @@ -58,7 +58,7 @@ RSpec.describe Ci::Processable, feature_category: :continuous_integration do let(:clone_accessors) do %i[pipeline project ref tag options name allow_failure stage stage_idx trigger_request yaml_variables when environment coverage_regex description tag_list protected needs_attributes job_variables_attributes - resource_group scheduling_type ci_stage partition_id id_tokens] + resource_group scheduling_type ci_stage partition_id id_tokens interruptible] end let(:reject_accessors) do @@ -76,7 +76,7 @@ RSpec.describe Ci::Processable, feature_category: :continuous_integration do job_artifacts_network_referee job_artifacts_dotenv job_artifacts_cobertura needs job_artifacts_accessibility job_artifacts_requirements job_artifacts_coverage_fuzzing - job_artifacts_requirements_v2 + job_artifacts_requirements_v2 job_artifacts_repository_xray job_artifacts_api_fuzzing terraform_state_versions job_artifacts_cyclonedx job_annotations job_artifacts_annotations].freeze end @@ -114,7 +114,8 @@ RSpec.describe Ci::Processable, feature_category: :continuous_integration do shared_examples_for 'clones the processable' do before_all do - processable.update!(stage: 'test', stage_id: stage.id) + processable.assign_attributes(stage: 'test', stage_id: stage.id, interruptible: true) + processable.save! create(:ci_build_need, build: processable) end @@ -187,7 +188,7 @@ RSpec.describe Ci::Processable, feature_category: :continuous_integration do Ci::Build.attribute_names.map(&:to_sym) + Ci::Build.attribute_aliases.keys.map(&:to_sym) + Ci::Build.reflect_on_all_associations.map(&:name) + - [:tag_list, :needs_attributes, :job_variables_attributes, :id_tokens] + [:tag_list, :needs_attributes, :job_variables_attributes, :id_tokens, :interruptible] current_accessors.uniq! diff --git a/spec/models/ci/runner_manager_build_spec.rb b/spec/models/ci/runner_manager_build_spec.rb index 3a381313b76..a4dd3a2c748 100644 --- a/spec/models/ci/runner_manager_build_spec.rb +++ b/spec/models/ci/runner_manager_build_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Ci::RunnerManagerBuild, model: true, feature_category: :runner_fleet do +RSpec.describe Ci::RunnerManagerBuild, model: true, feature_category: :fleet_visibility do let_it_be(:runner) { create(:ci_runner) } let_it_be(:runner_manager) { create(:ci_runner_machine, runner: runner) } let_it_be(:build) { create(:ci_build, runner_manager: runner_manager) } diff --git a/spec/models/ci/runner_manager_spec.rb b/spec/models/ci/runner_manager_spec.rb index 01275ffd31c..02a72afe0c6 100644 --- a/spec/models/ci/runner_manager_spec.rb +++ b/spec/models/ci/runner_manager_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Ci::RunnerManager, feature_category: :runner_fleet, type: :model do +RSpec.describe Ci::RunnerManager, feature_category: :fleet_visibility, type: :model do it_behaves_like 'having unique enum values' it_behaves_like 'it has loose foreign keys' do diff --git a/spec/models/ci/runner_version_spec.rb b/spec/models/ci/runner_version_spec.rb index bce1f2a6c39..32f840a8034 100644 --- a/spec/models/ci/runner_version_spec.rb +++ b/spec/models/ci/runner_version_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Ci::RunnerVersion, feature_category: :runner_fleet do +RSpec.describe Ci::RunnerVersion, feature_category: :fleet_visibility do let_it_be(:runner_version_upgrade_recommended) do create(:ci_runner_version, version: 'abc234', status: :recommended) end |