Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'spec/models/ci')
-rw-r--r--spec/models/ci/bridge_spec.rb220
-rw-r--r--spec/models/ci/build_need_spec.rb18
-rw-r--r--spec/models/ci/build_spec.rb36
-rw-r--r--spec/models/ci/catalog/components_project_spec.rb1
-rw-r--r--spec/models/ci/catalog/listing_spec.rb292
-rw-r--r--spec/models/ci/catalog/resource_spec.rb270
-rw-r--r--spec/models/ci/catalog/resources/sync_event_spec.rb190
-rw-r--r--spec/models/ci/catalog/resources/version_spec.rb50
-rw-r--r--spec/models/ci/job_artifact_spec.rb10
-rw-r--r--spec/models/ci/job_token/scope_spec.rb12
-rw-r--r--spec/models/ci/pipeline_metadata_spec.rb18
-rw-r--r--spec/models/ci/pipeline_spec.rb126
-rw-r--r--spec/models/ci/processable_spec.rb9
-rw-r--r--spec/models/ci/runner_manager_build_spec.rb2
-rw-r--r--spec/models/ci/runner_manager_spec.rb2
-rw-r--r--spec/models/ci/runner_version_spec.rb2
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