diff options
Diffstat (limited to 'spec/lib/gitlab/ci')
40 files changed, 1811 insertions, 453 deletions
diff --git a/spec/lib/gitlab/ci/build/image_spec.rb b/spec/lib/gitlab/ci/build/image_spec.rb index 4895077a731..f8c0d69be2e 100644 --- a/spec/lib/gitlab/ci/build/image_spec.rb +++ b/spec/lib/gitlab/ci/build/image_spec.rb @@ -10,7 +10,7 @@ RSpec.describe Gitlab::Ci::Build::Image do context 'when image is defined in job' do let(:image_name) { 'image:1.0' } - let(:job) { create(:ci_build, options: { image: image_name } ) } + let(:job) { create(:ci_build, options: { image: image_name }) } context 'when image is defined as string' do it 'fabricates an object of the proper class' do @@ -29,12 +29,14 @@ RSpec.describe Gitlab::Ci::Build::Image do context 'when image is defined as hash' do let(:entrypoint) { '/bin/sh' } let(:pull_policy) { %w[always if-not-present] } + let(:executor_opts) { { docker: { platform: 'arm64' } } } let(:job) do create(:ci_build, options: { image: { name: image_name, entrypoint: entrypoint, ports: [80], - pull_policy: pull_policy } } ) + executor_opts: executor_opts, + pull_policy: pull_policy } }) end it 'fabricates an object of the proper class' do @@ -44,6 +46,7 @@ RSpec.describe Gitlab::Ci::Build::Image do it 'populates fabricated object with the proper attributes' do expect(subject.name).to eq(image_name) expect(subject.entrypoint).to eq(entrypoint) + expect(subject.executor_opts).to eq(executor_opts) expect(subject.pull_policy).to eq(pull_policy) end @@ -98,11 +101,12 @@ RSpec.describe Gitlab::Ci::Build::Image do let(:service_entrypoint) { '/bin/sh' } let(:service_alias) { 'db' } let(:service_command) { 'sleep 30' } + let(:executor_opts) { { docker: { platform: 'amd64' } } } let(:pull_policy) { %w[always if-not-present] } let(:job) do create(:ci_build, options: { services: [{ name: service_image_name, entrypoint: service_entrypoint, alias: service_alias, command: service_command, ports: [80], - pull_policy: pull_policy }] }) + executor_opts: executor_opts, pull_policy: pull_policy }] }) end it 'fabricates an non-empty array of objects' do @@ -116,6 +120,7 @@ RSpec.describe Gitlab::Ci::Build::Image do expect(subject.first.entrypoint).to eq(service_entrypoint) expect(subject.first.alias).to eq(service_alias) expect(subject.first.command).to eq(service_command) + expect(subject.first.executor_opts).to eq(executor_opts) expect(subject.first.pull_policy).to eq(pull_policy) port = subject.first.ports.first diff --git a/spec/lib/gitlab/ci/components/instance_path_spec.rb b/spec/lib/gitlab/ci/components/instance_path_spec.rb index 4ba963b54b5..b9b4c3f7c69 100644 --- a/spec/lib/gitlab/ci/components/instance_path_spec.rb +++ b/spec/lib/gitlab/ci/components/instance_path_spec.rb @@ -42,48 +42,86 @@ RSpec.describe Gitlab::Ci::Components::InstancePath, feature_category: :pipeline end end - context 'when the component is simple (single file template)' do - it 'fetches the component content', :aggregate_failures do + shared_examples 'does not find the component' do + it 'returns nil' do result = path.fetch_content!(current_user: user) - expect(result.content).to eq('image: alpine_1') - expect(result.path).to eq('templates/secret-detection.yml') - expect(path.host).to eq(current_host) - expect(path.project).to eq(project) - expect(path.sha).to eq(project.commit('master').id) + expect(result).to be_nil + end + end + + shared_examples 'finds the component' do + shared_examples 'fetches the component content' do + it 'fetches the component content', :aggregate_failures do + result = path.fetch_content!(current_user: user) + expect(result.content).to eq(file_content) + expect(result.path).to eq(file_path) + expect(path.host).to eq(current_host) + expect(path.project).to eq(project) + expect(path.sha).to eq(project.commit('master').id) + end + end + + it_behaves_like 'fetches the component content' + + context 'when feature flag ci_redirect_component_project is disabled' do + before do + stub_feature_flags(ci_redirect_component_project: false) + end + + it_behaves_like 'fetches the component content' + end + + context 'when the there is a redirect set for the project' do + let!(:redirect_route) { project.redirect_routes.create!(path: 'another-group/new-project') } + let(:project_path) { redirect_route.path } + + it_behaves_like 'fetches the component content' + + context 'when feature flag ci_redirect_component_project is disabled' do + before do + stub_feature_flags(ci_redirect_component_project: false) + end + + it_behaves_like 'does not find the component' + end + end + end + + context 'when the component is simple (single file template)' do + it_behaves_like 'finds the component' do + let(:file_path) { 'templates/secret-detection.yml' } + let(:file_content) { 'image: alpine_1' } end end context 'when the component is complex (directory-based template)' do let(:address) { "acme.com/#{project_path}/dast@#{version}" } - it 'fetches the component content', :aggregate_failures do - result = path.fetch_content!(current_user: user) - expect(result.content).to eq('image: alpine_2') - expect(result.path).to eq('templates/dast/template.yml') - expect(path.host).to eq(current_host) - expect(path.project).to eq(project) - expect(path.sha).to eq(project.commit('master').id) + it_behaves_like 'finds the component' do + let(:file_path) { 'templates/dast/template.yml' } + let(:file_content) { 'image: alpine_2' } end context 'when there is an invalid nested component folder' do let(:address) { "acme.com/#{project_path}/dast/another-folder@#{version}" } - it 'returns nil' do - result = path.fetch_content!(current_user: user) - expect(result.content).to be_nil - end + it_behaves_like 'does not find the component' end context 'when there is an invalid nested component path' do let(:address) { "acme.com/#{project_path}/dast/another-template@#{version}" } - it 'returns nil' do - result = path.fetch_content!(current_user: user) - expect(result.content).to be_nil - end + it_behaves_like 'does not find the component' end end + context "when the project path starts with '/'" do + let(:project_path) { "/#{project.full_path}" } + + it_behaves_like 'does not find the component' + end + + # TODO: remove when deleting the feature flag `ci_redirect_component_project` shared_examples 'prevents infinite loop' do |prefix| context "when the project path starts with '#{prefix}'" do let(:project_path) { "#{prefix}#{project.full_path}" } @@ -127,7 +165,7 @@ RSpec.describe Gitlab::Ci::Components::InstancePath, feature_category: :pipeline released_at: Time.zone.now) end - it 'fetches the component content', :aggregate_failures do + it 'returns the component content of the latest project release', :aggregate_failures do result = path.fetch_content!(current_user: user) expect(result.content).to eq('image: alpine_2') expect(result.path).to eq('templates/secret-detection.yml') @@ -135,6 +173,25 @@ RSpec.describe Gitlab::Ci::Components::InstancePath, feature_category: :pipeline expect(path.project).to eq(project) expect(path.sha).to eq(latest_sha) end + + context 'when the project is a catalog resource' do + let_it_be(:resource) { create(:ci_catalog_resource, project: project) } + + before do + project.releases.each do |release| + create(:ci_catalog_resource_version, catalog_resource: resource, release: release) + end + end + + it 'returns the component content of the latest catalog resource version', :aggregate_failures do + result = path.fetch_content!(current_user: user) + expect(result.content).to eq('image: alpine_2') + expect(result.path).to eq('templates/secret-detection.yml') + expect(path.host).to eq(current_host) + expect(path.project).to eq(project) + expect(path.sha).to eq(latest_sha) + end + end end context 'when version does not exist' do @@ -162,88 +219,5 @@ RSpec.describe Gitlab::Ci::Components::InstancePath, feature_category: :pipeline end end end - - # All the following tests are for deprecated code and will be removed - # in https://gitlab.com/gitlab-org/gitlab/-/issues/415855 - context 'when the project does not contain a templates directory' do - let(:project_path) { project.full_path } - let(:address) { "acme.com/#{project_path}/component@#{version}" } - - let_it_be(:project) do - create( - :project, :custom_repo, - files: { - 'component/template.yml' => 'image: alpine' - } - ) - end - - before do - project.add_developer(user) - end - - it 'fetches the component content', :aggregate_failures do - result = path.fetch_content!(current_user: user) - expect(result.content).to eq('image: alpine') - expect(result.path).to eq('component/template.yml') - expect(path.host).to eq(current_host) - expect(path.project).to eq(project) - expect(path.sha).to eq(project.commit('master').id) - end - - context 'when project path is nested under a subgroup' do - let_it_be(:group) { create(:group, :nested) } - let_it_be(:project) do - create( - :project, :custom_repo, - files: { - 'component/template.yml' => 'image: alpine' - }, - group: group - ) - end - - it 'fetches the component content', :aggregate_failures do - result = path.fetch_content!(current_user: user) - expect(result.content).to eq('image: alpine') - expect(result.path).to eq('component/template.yml') - expect(path.host).to eq(current_host) - expect(path.project).to eq(project) - expect(path.sha).to eq(project.commit('master').id) - end - end - - context 'when current GitLab instance is installed on a relative URL' do - let(:address) { "acme.com/gitlab/#{project_path}/component@#{version}" } - let(:current_host) { 'acme.com/gitlab/' } - - it 'fetches the component content', :aggregate_failures do - result = path.fetch_content!(current_user: user) - expect(result.content).to eq('image: alpine') - expect(result.path).to eq('component/template.yml') - expect(path.host).to eq(current_host) - expect(path.project).to eq(project) - expect(path.sha).to eq(project.commit('master').id) - end - end - - context 'when version does not exist' do - let(:version) { 'non-existent' } - - it 'returns nil', :aggregate_failures do - expect(path.fetch_content!(current_user: user)).to be_nil - expect(path.host).to eq(current_host) - expect(path.project).to eq(project) - expect(path.sha).to be_nil - end - end - - context 'when user does not have permissions' do - it 'raises an error when fetching the content' do - expect { path.fetch_content!(current_user: build(:user)) } - .to raise_error(Gitlab::Access::AccessDeniedError) - end - end - end end end diff --git a/spec/lib/gitlab/ci/config/entry/auto_cancel_spec.rb b/spec/lib/gitlab/ci/config/entry/auto_cancel_spec.rb new file mode 100644 index 00000000000..bdd66cc00a1 --- /dev/null +++ b/spec/lib/gitlab/ci/config/entry/auto_cancel_spec.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Config::Entry::AutoCancel, feature_category: :pipeline_composition do + subject(:config) { described_class.new(config_hash) } + + context 'with on_new_commit' do + let(:config_hash) do + { on_new_commit: 'interruptible' } + end + + it { is_expected.to be_valid } + + it 'returns value correctly' do + expect(config.value).to eq(config_hash) + end + + context 'when on_new_commit is invalid' do + let(:config_hash) do + { on_new_commit: 'invalid' } + end + + it { is_expected.not_to be_valid } + + it 'returns errors' do + expect(config.errors) + .to include('auto cancel on new commit must be one of: conservative, interruptible, disabled') + end + end + end + + context 'with on_job_failure' do + ['all', 'none', nil].each do |value| + context 'when the `on_job_failure` value is valid' do + let(:config_hash) { { on_job_failure: value } } + + it { is_expected.to be_valid } + + it 'returns value correctly' do + expect(config.value).to eq(on_job_failure: value) + end + end + end + + context 'when on_job_failure is invalid' do + let(:config_hash) do + { on_job_failure: 'invalid' } + end + + it { is_expected.not_to be_valid } + + it 'returns errors' do + expect(config.errors) + .to include('auto cancel on job failure must be one of: none, all') + end + end + end + + context 'with invalid key' do + let(:config_hash) do + { invalid: 'interruptible' } + end + + it { is_expected.not_to be_valid } + + it 'returns errors' do + expect(config.errors) + .to include('auto cancel config contains unknown keys: invalid') + end + end +end diff --git a/spec/lib/gitlab/ci/config/entry/bridge_spec.rb b/spec/lib/gitlab/ci/config/entry/bridge_spec.rb index 6e6b9d949c5..35f2a99ee87 100644 --- a/spec/lib/gitlab/ci/config/entry/bridge_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/bridge_spec.rb @@ -2,10 +2,11 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::Config::Entry::Bridge do +RSpec.describe Gitlab::Ci::Config::Entry::Bridge, feature_category: :continuous_integration do subject(:entry) { described_class.new(config, name: :my_bridge) } it_behaves_like 'with inheritable CI config' do + let(:config) { { trigger: 'some/project' } } let(:inheritable_key) { 'default' } let(:inheritable_class) { Gitlab::Ci::Config::Entry::Default } @@ -13,9 +14,13 @@ RSpec.describe Gitlab::Ci::Config::Entry::Bridge do # that we know that we don't want to inherit # as they do not have sense in context of Bridge let(:ignored_inheritable_columns) do - %i[before_script after_script hooks image services cache interruptible timeout + %i[before_script after_script hooks image services cache timeout retry tags artifacts id_tokens] end + + before do + allow(entry).to receive_message_chain(:inherit_entry, :default_entry, :inherit?).and_return(true) + end end describe '.matching?' do diff --git a/spec/lib/gitlab/ci/config/entry/image_spec.rb b/spec/lib/gitlab/ci/config/entry/image_spec.rb index 17c45ec4c2c..99a6e25b313 100644 --- a/spec/lib/gitlab/ci/config/entry/image_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/image_spec.rb @@ -42,6 +42,12 @@ RSpec.describe Gitlab::Ci::Config::Entry::Image do end end + describe '#executor_opts' do + it "returns nil" do + expect(entry.executor_opts).to be_nil + end + end + describe '#ports' do it "returns image's ports" do expect(entry.ports).to be_nil @@ -88,6 +94,54 @@ RSpec.describe Gitlab::Ci::Config::Entry::Image do end end + context 'when configuration specifies docker' do + let(:config) { { name: 'image:1.0', docker: {} } } + + it 'is valid' do + expect(entry).to be_valid + end + + describe '#value' do + it "returns value" do + expect(entry.value).to eq( + name: 'image:1.0', + executor_opts: { + docker: {} + } + ) + end + end + + context "when docker specifies an option" do + let(:config) { { name: 'image:1.0', docker: { platform: 'amd64' } } } + + it 'is valid' do + expect(entry).to be_valid + end + + describe '#value' do + it "returns value" do + expect(entry.value).to eq( + name: 'image:1.0', + executor_opts: { + docker: { platform: 'amd64' } + } + ) + end + end + end + + context "when docker specifies an invalid option" do + let(:config) { { name: 'image:1.0', docker: { platform: 1 } } } + + it 'is not valid' do + expect(entry).not_to be_valid + expect(entry.errors.first) + .to match %r{image executor opts '/docker/platform' must be a valid 'string'} + end + end + end + context 'when configuration has ports' do let(:ports) { [{ number: 80, protocol: 'http', name: 'foobar' }] } let(:config) { { name: 'image:1.0', entrypoint: %w[/bin/sh run], ports: ports } } @@ -146,7 +200,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Image do describe '#errors' do it 'saves errors' do expect(entry.errors.first) - .to match /config should be a hash or a string/ + .to match(/config should be a hash or a string/) end end @@ -163,7 +217,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Image do describe '#errors' do it 'saves errors' do expect(entry.errors.first) - .to match /config contains unknown keys: non_existing/ + .to match(/config contains unknown keys: non_existing/) end end diff --git a/spec/lib/gitlab/ci/config/entry/includes_spec.rb b/spec/lib/gitlab/ci/config/entry/includes_spec.rb index f1f28c24e70..54c02868010 100644 --- a/spec/lib/gitlab/ci/config/entry/includes_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/includes_spec.rb @@ -13,4 +13,18 @@ RSpec.describe ::Gitlab::Ci::Config::Entry::Includes, feature_category: :pipelin 2.times { expect { described_class.new(config) }.not_to change { described_class.aspects.count } } end end + + describe 'validations' do + let(:config) { [1, 2] } + + let(:includes_entry) { described_class.new(config, max_size: 1) } + + it 'returns invalid' do + expect(includes_entry).not_to be_valid + end + + it 'returns the appropriate error' do + expect(includes_entry.errors).to include('includes config is too long (maximum is 1)') + end + end end diff --git a/spec/lib/gitlab/ci/config/entry/inherit/default_spec.rb b/spec/lib/gitlab/ci/config/entry/inherit/default_spec.rb index 7cd9b0acb99..c0d21385ce6 100644 --- a/spec/lib/gitlab/ci/config/entry/inherit/default_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/inherit/default_spec.rb @@ -31,6 +31,7 @@ RSpec.describe ::Gitlab::Ci::Config::Entry::Inherit::Default do false | false %w[image] | true %w[before_script] | false + '123' | false end with_them do diff --git a/spec/lib/gitlab/ci/config/entry/job_spec.rb b/spec/lib/gitlab/ci/config/entry/job_spec.rb index 24d3cac6616..073d8feaadd 100644 --- a/spec/lib/gitlab/ci/config/entry/job_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/job_spec.rb @@ -789,7 +789,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Job, feature_category: :pipeline_compo hooks: { pre_get_sources_script: 'echo hello' } } end - it 'returns correct value' do + it 'returns correct values' do expect(entry.value).to eq( name: :rspec, before_script: %w[ls pwd], @@ -806,6 +806,93 @@ RSpec.describe Gitlab::Ci::Config::Entry::Job, feature_category: :pipeline_compo ) end end + + context 'with retry present in the config' do + let(:config) do + { + script: 'rspec', + retry: { max: 1, when: "always" } + } + end + + it 'returns correct values' do + expect(entry.value) + .to eq(name: :rspec, + script: %w[rspec], + stage: 'test', + ignore: false, + retry: { max: 1, when: %w[always] }, + only: { refs: %w[branches tags] }, + job_variables: {}, + root_variables_inheritance: true, + scheduling_type: :stage + ) + end + + context 'when ci_retry_on_exit_codes feature flag is disabled' do + before do + stub_feature_flags(ci_retry_on_exit_codes: false) + end + + it 'returns correct values' do + expect(entry.value) + .to eq(name: :rspec, + script: %w[rspec], + stage: 'test', + ignore: false, + retry: { max: 1, when: %w[always] }, + only: { refs: %w[branches tags] }, + job_variables: {}, + root_variables_inheritance: true, + scheduling_type: :stage + ) + end + end + + context 'with exit_codes present' do + let(:config) do + { + script: 'rspec', + retry: { max: 1, when: "always", exit_codes: 255 } + } + end + + it 'returns correct values' do + expect(entry.value) + .to eq(name: :rspec, + script: %w[rspec], + stage: 'test', + ignore: false, + retry: { max: 1, when: %w[always], exit_codes: [255] }, + only: { refs: %w[branches tags] }, + job_variables: {}, + root_variables_inheritance: true, + scheduling_type: :stage + ) + end + + context 'when ci_retry_on_exit_codes feature flag is disabled' do + before do + stub_feature_flags(ci_retry_on_exit_codes: false) + end + + it 'returns correct values' do + expect(entry.value) + .to eq(name: :rspec, + script: %w[rspec], + stage: 'test', + ignore: false, + # Shouldn't include exit_codes + retry: { max: 1, when: %w[always] }, + only: { refs: %w[branches tags] }, + job_variables: {}, + root_variables_inheritance: true, + scheduling_type: :stage + ) + end + end + end + end end context 'when job is using tags' do diff --git a/spec/lib/gitlab/ci/config/entry/processable_spec.rb b/spec/lib/gitlab/ci/config/entry/processable_spec.rb index 44e2fdbac37..84a8fd827cb 100644 --- a/spec/lib/gitlab/ci/config/entry/processable_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/processable_spec.rb @@ -217,6 +217,15 @@ RSpec.describe Gitlab::Ci::Config::Entry::Processable, feature_category: :pipeli end end end + + context 'when interruptible is not a boolean' do + let(:config) { { interruptible: 123 } } + + it 'returns error about wrong value type' do + expect(entry).not_to be_valid + expect(entry.errors).to include "interruptible config should be a boolean value" + end + end end describe '#relevant?' do @@ -462,6 +471,28 @@ RSpec.describe Gitlab::Ci::Config::Entry::Processable, feature_category: :pipeli end end end + + context 'with interruptible' do + context 'when interruptible is not defined' do + let(:config) { { script: 'ls' } } + + it 'sets interruptible to nil' do + entry.compose!(deps) + + expect(entry.value[:interruptible]).to be_nil + end + end + + context 'when interruptible is defined' do + let(:config) { { script: 'ls', interruptible: true } } + + it 'sets interruptible to the value' do + entry.compose!(deps) + + expect(entry.value[:interruptible]).to eq(true) + end + end + end end context 'when composed' do diff --git a/spec/lib/gitlab/ci/config/entry/reports_spec.rb b/spec/lib/gitlab/ci/config/entry/reports_spec.rb index d610c3ce2f6..a6675229c62 100644 --- a/spec/lib/gitlab/ci/config/entry/reports_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/reports_spec.rb @@ -49,6 +49,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Reports, feature_category: :pipeline_c :accessibility | 'gl-accessibility.json' :cyclonedx | 'gl-sbom.cdx.zip' :annotations | 'gl-annotations.json' + :repository_xray | 'gl-repository-xray.json' end with_them do diff --git a/spec/lib/gitlab/ci/config/entry/retry_spec.rb b/spec/lib/gitlab/ci/config/entry/retry_spec.rb index 84ef5344a8b..e01b50c5fbd 100644 --- a/spec/lib/gitlab/ci/config/entry/retry_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/retry_spec.rb @@ -11,8 +11,9 @@ RSpec.describe Gitlab::Ci::Config::Entry::Retry do end shared_context 'when retry value is a hash', :hash do - let(:config) { { max: max, when: public_send(:when) }.compact } + let(:config) { { max: max, when: public_send(:when), exit_codes: public_send(:exit_codes) }.compact } let(:when) {} + let(:exit_codes) {} let(:max) {} end @@ -43,6 +44,44 @@ RSpec.describe Gitlab::Ci::Config::Entry::Retry do expect(value).to eq(when: %w[unknown_failure runner_system_failure]) end end + + context 'and `exit_codes` is an integer' do + let(:exit_codes) { 255 } + + it 'returns an array of exit_codes' do + expect(value).to eq(exit_codes: [255]) + end + end + + context 'and `exit_codes` is an array' do + let(:exit_codes) { [255, 142] } + + it 'returns an array of exit_codes' do + expect(value).to eq(exit_codes: [255, 142]) + end + end + end + + context 'when ci_retry_on_exit_codes feature flag is disabled', :hash do + before do + stub_feature_flags(ci_retry_on_exit_codes: false) + end + + context 'when `exit_codes` is an integer' do + let(:exit_codes) { 255 } + + it 'deletes the attribute exit_codes' do + expect(value).to eq({}) + end + end + + context 'when `exit_codes` is an array' do + let(:exit_codes) { [255, 137] } + + it 'deletes the attribute exit_codes' do + expect(value).to eq({}) + end + end end end @@ -65,6 +104,22 @@ RSpec.describe Gitlab::Ci::Config::Entry::Retry do end end + context 'with numeric exit_codes' do + let(:exit_codes) { 255 } + + it 'is valid' do + expect(entry).to be_valid + end + end + + context 'with hash values exit_codes' do + let(:exit_codes) { [255, 142] } + + it 'is valid' do + expect(entry).to be_valid + end + end + context 'with string when' do let(:when) { 'unknown_failure' } @@ -202,7 +257,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Retry do end end - context 'iwth max too high' do + context 'with max too high' do let(:max) { 10 } it 'returns error about value too high' do @@ -211,6 +266,33 @@ RSpec.describe Gitlab::Ci::Config::Entry::Retry do end end + context 'with exit_codes in wrong format' do + let(:exit_codes) { true } + + it 'raises an error' do + expect(entry).not_to be_valid + expect(entry.errors).to include 'retry exit codes should be an array of integers or an integer' + end + end + + context 'with exit_codes in wrong array format' do + let(:exit_codes) { ['string 1', 'string 2'] } + + it 'raises an error' do + expect(entry).not_to be_valid + expect(entry.errors).to include 'retry exit codes should be an array of integers or an integer' + end + end + + context 'with exit_codes in wrong mixed array format' do + let(:exit_codes) { [255, '155'] } + + it 'raises an error' do + expect(entry).not_to be_valid + expect(entry.errors).to include 'retry exit codes should be an array of integers or an integer' + end + end + context 'with when in wrong format' do let(:when) { true } diff --git a/spec/lib/gitlab/ci/config/entry/service_spec.rb b/spec/lib/gitlab/ci/config/entry/service_spec.rb index 1f935bebed5..82747e7b521 100644 --- a/spec/lib/gitlab/ci/config/entry/service_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/service_spec.rb @@ -47,11 +47,23 @@ RSpec.describe Gitlab::Ci::Config::Entry::Service do expect(entry.ports).to be_nil end end + + describe '#executor_opts' do + it "returns service's executor_opts configuration" do + expect(entry.executor_opts).to be_nil + end + end end context 'when configuration is a hash' do let(:config) do - { name: 'postgresql:9.5', alias: 'db', command: %w[cmd run], entrypoint: %w[/bin/sh run] } + { + name: 'postgresql:9.5', + alias: 'db', + command: %w[cmd run], + entrypoint: %w[/bin/sh run], + variables: { 'MY_VAR' => 'variable' } + } end describe '#valid?' do @@ -141,6 +153,51 @@ RSpec.describe Gitlab::Ci::Config::Entry::Service do end end + context 'when configuration has docker options' do + let(:config) { { name: 'postgresql:9.5', docker: { platform: 'amd64' } } } + + describe '#valid?' do + it 'is valid' do + expect(entry).to be_valid + end + end + + describe '#value' do + it "returns value" do + expect(entry.value).to eq( + name: 'postgresql:9.5', + executor_opts: { + docker: { platform: 'amd64' } + } + ) + end + end + end + + context 'when docker options have an invalid property' do + let(:config) { { name: 'postgresql:9.5', docker: { invalid: 'option' } } } + + describe '#valid?' do + it 'is not valid' do + expect(entry).not_to be_valid + expect(entry.errors.first) + .to match %r{service executor opts '/docker/invalid' must be a valid 'schema'} + end + end + end + + context 'when docker options platform is not string' do + let(:config) { { name: 'postgresql:9.5', docker: { platform: 123 } } } + + describe '#valid?' do + it 'is not valid' do + expect(entry).not_to be_valid + expect(entry.errors.first) + .to match %r{service executor opts '/docker/platform' must be a valid 'string'} + end + end + end + context 'when configuration has pull_policy' do let(:config) { { name: 'postgresql:9.5', pull_policy: 'if-not-present' } } diff --git a/spec/lib/gitlab/ci/config/entry/workflow_spec.rb b/spec/lib/gitlab/ci/config/entry/workflow_spec.rb index 97ac199f47d..d3ce3ffe641 100644 --- a/spec/lib/gitlab/ci/config/entry/workflow_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/workflow_spec.rb @@ -2,13 +2,12 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::Config::Entry::Workflow do - let(:factory) { Gitlab::Config::Entry::Factory.new(described_class).value(rules_hash) } - let(:config) { factory.create! } +RSpec.describe Gitlab::Ci::Config::Entry::Workflow, feature_category: :pipeline_composition do + subject(:config) { described_class.new(workflow_hash) } describe 'validations' do context 'when work config value is a string' do - let(:rules_hash) { 'build' } + let(:workflow_hash) { 'build' } describe '#valid?' do it 'is invalid' do @@ -22,13 +21,13 @@ RSpec.describe Gitlab::Ci::Config::Entry::Workflow do describe '#value' do it 'returns the invalid configuration' do - expect(config.value).to eq(rules_hash) + expect(config.value).to eq(workflow_hash) end end end context 'when work config value is a hash' do - let(:rules_hash) { { rules: [{ if: '$VAR' }] } } + let(:workflow_hash) { { rules: [{ if: '$VAR' }] } } describe '#valid?' do it 'is valid' do @@ -42,12 +41,12 @@ RSpec.describe Gitlab::Ci::Config::Entry::Workflow do describe '#value' do it 'returns the config' do - expect(config.value).to eq(rules_hash) + expect(config.value).to eq(workflow_hash) end end context 'with an invalid key' do - let(:rules_hash) { { trash: [{ if: '$VAR' }] } } + let(:workflow_hash) { { trash: [{ if: '$VAR' }] } } describe '#valid?' do it 'is invalid' do @@ -61,64 +60,79 @@ RSpec.describe Gitlab::Ci::Config::Entry::Workflow do describe '#value' do it 'returns the invalid configuration' do - expect(config.value).to eq(rules_hash) + expect(config.value).to eq(workflow_hash) end end end + end + end - context 'with workflow name' do - let(:factory) { Gitlab::Config::Entry::Factory.new(described_class).value(workflow_hash) } + describe '.default' do + it 'is nil' do + expect(described_class.default).to be_nil + end + end - context 'with a blank name' do - let(:workflow_hash) do - { name: '' } - end + context 'with workflow name' do + context 'with a blank name' do + let(:workflow_hash) do + { name: '' } + end - it 'is invalid' do - expect(config).not_to be_valid - end + it 'is invalid' do + expect(config).not_to be_valid + end - it 'returns error about invalid name' do - expect(config.errors).to include('workflow name is too short (minimum is 1 character)') - end - end + it 'returns error about invalid name' do + expect(config.errors).to include('workflow name is too short (minimum is 1 character)') + end + end - context 'with too long name' do - let(:workflow_hash) do - { name: 'a' * 256 } - end + context 'with too long name' do + let(:workflow_hash) do + { name: 'a' * 256 } + end - it 'is invalid' do - expect(config).not_to be_valid - end + it 'is invalid' do + expect(config).not_to be_valid + end - it 'returns error about invalid name' do - expect(config.errors).to include('workflow name is too long (maximum is 255 characters)') - end - end + it 'returns error about invalid name' do + expect(config.errors).to include('workflow name is too long (maximum is 255 characters)') + end + end - context 'when name is nil' do - let(:workflow_hash) { { name: nil } } + context 'when name is nil' do + let(:workflow_hash) { { name: nil } } - it 'is valid' do - expect(config).to be_valid - end - end + it 'is valid' do + expect(config).to be_valid + end + end - context 'when name is not provided' do - let(:workflow_hash) { { rules: [{ if: '$VAR' }] } } + context 'when name is not provided' do + let(:workflow_hash) { { rules: [{ if: '$VAR' }] } } - it 'is valid' do - expect(config).to be_valid - end - end + it 'is valid' do + expect(config).to be_valid end end end - describe '.default' do - it 'is nil' do - expect(described_class.default).to be_nil + context 'with auto_cancel' do + let(:workflow_hash) do + { + auto_cancel: { + on_new_commit: 'interruptible', + on_job_failure: 'none' + } + } + end + + it { is_expected.to be_valid } + + it 'returns value correctly' do + expect(config.value).to eq(workflow_hash) end end end diff --git a/spec/lib/gitlab/ci/config/external/file/local_spec.rb b/spec/lib/gitlab/ci/config/external/file/local_spec.rb index 0643bf0c046..b961ee0d190 100644 --- a/spec/lib/gitlab/ci/config/external/file/local_spec.rb +++ b/spec/lib/gitlab/ci/config/external/file/local_spec.rb @@ -269,8 +269,8 @@ RSpec.describe Gitlab::Ci::Config::External::File::Local, feature_category: :pip context_sha: sha, type: :local, location: 'lib/gitlab/ci/templates/existent-file.yml', - blob: "http://localhost/#{project.full_path}/-/blob/#{sha}/lib/gitlab/ci/templates/existent-file.yml", - raw: "http://localhost/#{project.full_path}/-/raw/#{sha}/lib/gitlab/ci/templates/existent-file.yml", + blob: "http://#{Gitlab.config.gitlab.host}/#{project.full_path}/-/blob/#{sha}/lib/gitlab/ci/templates/existent-file.yml", + raw: "http://#{Gitlab.config.gitlab.host}/#{project.full_path}/-/raw/#{sha}/lib/gitlab/ci/templates/existent-file.yml", extra: {} ) } diff --git a/spec/lib/gitlab/ci/config/external/file/remote_spec.rb b/spec/lib/gitlab/ci/config/external/file/remote_spec.rb index f8d3d1019f5..7293e640112 100644 --- a/spec/lib/gitlab/ci/config/external/file/remote_spec.rb +++ b/spec/lib/gitlab/ci/config/external/file/remote_spec.rb @@ -75,7 +75,9 @@ RSpec.describe Gitlab::Ci::Config::External::File::Remote, feature_category: :pi context 'with a timeout' do before do - allow(Gitlab::HTTP).to receive(:get).and_raise(Timeout::Error) + allow_next_instance_of(HTTParty::Request) do |instance| + allow(instance).to receive(:perform).and_raise(Timeout::Error) + end end it { is_expected.to be_falsy } @@ -94,24 +96,33 @@ RSpec.describe Gitlab::Ci::Config::External::File::Remote, feature_category: :pi end end - describe "#content" do + # When the FF ci_parallel_remote_includes is removed, + # convert this `shared_context` to `describe` and remove `rubocop:disable`. + shared_context "#content" do # rubocop:disable RSpec/ContextWording -- This is temporary until the FF is removed. + subject(:content) do + remote_file.preload_content + remote_file.content + end + context 'with a valid remote file' do before do stub_full_request(location).to_return(body: remote_file_content) end it 'returns the content of the file' do - expect(remote_file.content).to eql(remote_file_content) + expect(content).to eql(remote_file_content) end end context 'with a timeout' do before do - allow(Gitlab::HTTP).to receive(:get).and_raise(Timeout::Error) + allow_next_instance_of(HTTParty::Request) do |instance| + allow(instance).to receive(:perform).and_raise(Timeout::Error) + end end it 'is falsy' do - expect(remote_file.content).to be_falsy + expect(content).to be_falsy end end @@ -123,7 +134,7 @@ RSpec.describe Gitlab::Ci::Config::External::File::Remote, feature_category: :pi end it 'is nil' do - expect(remote_file.content).to be_nil + expect(content).to be_nil end end @@ -131,11 +142,21 @@ RSpec.describe Gitlab::Ci::Config::External::File::Remote, feature_category: :pi let(:location) { 'http://localhost:8080' } it 'is nil' do - expect(remote_file.content).to be_nil + expect(content).to be_nil end end end + it_behaves_like "#content" + + context 'when the FF ci_parallel_remote_includes is disabled' do + before do + stub_feature_flags(ci_parallel_remote_includes: false) + end + + it_behaves_like "#content" + end + describe "#error_message" do subject(:error_message) do Gitlab::Ci::Config::External::Mapper::Verifier.new(context).process([remote_file]) @@ -234,13 +255,18 @@ RSpec.describe Gitlab::Ci::Config::External::File::Remote, feature_category: :pi end describe '#to_hash' do + subject(:to_hash) do + remote_file.preload_content + remote_file.to_hash + end + before do stub_full_request(location).to_return(body: remote_file_content) end context 'with a valid remote file' do it 'returns the content as a hash' do - expect(remote_file.to_hash).to eql( + expect(to_hash).to eql( before_script: ["apt-get update -qq && apt-get install -y -qq sqlite3 libsqlite3-dev nodejs", "ruby -v", "which ruby", @@ -260,7 +286,7 @@ RSpec.describe Gitlab::Ci::Config::External::File::Remote, feature_category: :pi end it 'returns the content as a hash' do - expect(remote_file.to_hash).to eql( + expect(to_hash).to eql( include: [ { local: 'another-file.yml', rules: [{ exists: ['Dockerfile'] }] } @@ -293,7 +319,7 @@ RSpec.describe Gitlab::Ci::Config::External::File::Remote, feature_category: :pi it 'returns the content as a hash' do expect(remote_file).to be_valid - expect(remote_file.to_hash).to eql( + expect(to_hash).to eql( include: [ { local: 'some-file.yml', rules: [{ exists: ['Dockerfile'] }] } diff --git a/spec/lib/gitlab/ci/config/external/mapper_spec.rb b/spec/lib/gitlab/ci/config/external/mapper_spec.rb index 5f28b45496f..d67b0ff8895 100644 --- a/spec/lib/gitlab/ci/config/external/mapper_spec.rb +++ b/spec/lib/gitlab/ci/config/external/mapper_spec.rb @@ -85,7 +85,13 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper, feature_category: :pipeline an_instance_of(Gitlab::Ci::Config::External::File::Remote)) end - it_behaves_like 'logging config file fetch', 'config_file_fetch_remote_content_duration_s', 1 + context 'when the FF ci_parallel_remote_includes is disabled' do + before do + stub_feature_flags(ci_parallel_remote_includes: false) + end + + it_behaves_like 'logging config file fetch', 'config_file_fetch_remote_content_duration_s', 1 + end end context 'when the key is a remote file hash' do diff --git a/spec/lib/gitlab/ci/config/external/processor_spec.rb b/spec/lib/gitlab/ci/config/external/processor_spec.rb index 68cdf56f198..4684495fa26 100644 --- a/spec/lib/gitlab/ci/config/external/processor_spec.rb +++ b/spec/lib/gitlab/ci/config/external/processor_spec.rb @@ -410,7 +410,7 @@ RSpec.describe Gitlab::Ci::Config::External::Processor, feature_category: :pipel let(:other_project_files) do { - '/component-x/template.yml' => <<~YAML + '/templates/component-x/template.yml' => <<~YAML component_x_job: script: echo Component X YAML diff --git a/spec/lib/gitlab/ci/config/interpolation/inputs/base_input_spec.rb b/spec/lib/gitlab/ci/config/interpolation/inputs/base_input_spec.rb index 30036ee68ed..b0a514cb1e2 100644 --- a/spec/lib/gitlab/ci/config/interpolation/inputs/base_input_spec.rb +++ b/spec/lib/gitlab/ci/config/interpolation/inputs/base_input_spec.rb @@ -4,8 +4,34 @@ require 'spec_helper' RSpec.describe Gitlab::Ci::Config::Interpolation::Inputs::BaseInput, feature_category: :pipeline_composition do describe '.matches?' do - it 'is not implemented' do - expect { described_class.matches?(double) }.to raise_error(NotImplementedError) + context 'when given is a hash' do + before do + stub_const('TestInput', Class.new(described_class)) + + TestInput.class_eval do + def self.type_name + 'test' + end + end + end + + context 'when the spec type matches the input type' do + it 'returns true' do + expect(TestInput.matches?({ type: 'test' })).to be_truthy + end + end + + context 'when the spec type does not match the input type' do + it 'returns false' do + expect(TestInput.matches?({ type: 'string' })).to be_falsey + end + end + end + + context 'when not given a hash' do + it 'returns false' do + expect(described_class.matches?([])).to be_falsey + end end end diff --git a/spec/lib/gitlab/ci/config/interpolation/text_interpolator_spec.rb b/spec/lib/gitlab/ci/config/interpolation/text_interpolator_spec.rb new file mode 100644 index 00000000000..70858c0fff8 --- /dev/null +++ b/spec/lib/gitlab/ci/config/interpolation/text_interpolator_spec.rb @@ -0,0 +1,221 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Config::Interpolation::TextInterpolator, feature_category: :pipeline_composition do + let(:result) { ::Gitlab::Ci::Config::Yaml::Result.new(config: [header, content]) } + + subject(:interpolator) { described_class.new(result, arguments, []) } + + context 'when input data is valid' do + let(:header) do + { spec: { inputs: { website: nil } } } + end + + let(:content) do + "test: 'deploy $[[ inputs.website ]]'" + end + + let(:arguments) do + { website: 'gitlab.com' } + end + + it 'correctly interpolates the config' do + interpolator.interpolate! + + expect(interpolator).to be_interpolated + expect(interpolator).to be_valid + expect(interpolator.to_result).to eq("test: 'deploy gitlab.com'") + end + end + + context 'when config has a syntax error' do + let(:result) { ::Gitlab::Ci::Config::Yaml::Result.new(error: 'Invalid configuration format') } + + let(:arguments) do + { website: 'gitlab.com' } + end + + it 'surfaces an error about invalid config' do + interpolator.interpolate! + + expect(interpolator).not_to be_valid + expect(interpolator.error_message).to eq('Invalid configuration format') + end + end + + context 'when spec header is missing but inputs are specified' do + let(:header) { nil } + let(:content) { "test: 'echo'" } + let(:arguments) { { foo: 'bar' } } + + it 'surfaces an error about invalid inputs' do + interpolator.interpolate! + + expect(interpolator).not_to be_valid + expect(interpolator.error_message).to eq( + 'Given inputs not defined in the `spec` section of the included configuration file' + ) + end + end + + context 'when spec header is invalid' do + let(:header) do + { spec: { arguments: { website: nil } } } + end + + let(:content) do + "test: 'deploy $[[ inputs.website ]]'" + end + + let(:arguments) do + { website: 'gitlab.com' } + end + + it 'surfaces an error about invalid header' do + interpolator.interpolate! + + expect(interpolator).not_to be_valid + expect(interpolator.error_message).to eq('header:spec config contains unknown keys: arguments') + end + end + + context 'when provided interpolation argument is invalid' do + let(:header) do + { spec: { inputs: { website: nil } } } + end + + let(:content) do + "test: 'deploy $[[ inputs.website ]]'" + end + + let(:arguments) do + { website: ['gitlab.com'] } + end + + it 'returns an error about the invalid argument' do + interpolator.interpolate! + + expect(interpolator).not_to be_valid + expect(interpolator.error_message).to eq('`website` input: provided value is not a string') + end + end + + context 'when interpolation block is invalid' do + let(:header) do + { spec: { inputs: { website: nil } } } + end + + let(:content) do + "test: 'deploy $[[ inputs.abc ]]'" + end + + let(:arguments) do + { website: 'gitlab.com' } + end + + it 'returns an error about the invalid block' do + interpolator.interpolate! + + expect(interpolator).not_to be_valid + expect(interpolator.error_message).to eq('unknown interpolation key: `abc`') + end + end + + context 'when multiple interpolation blocks are invalid' do + let(:header) do + { spec: { inputs: { website: nil } } } + end + + let(:content) do + "test: 'deploy $[[ inputs.something.abc ]] $[[ inputs.cde ]] $[[ efg ]]'" + end + + let(:arguments) do + { website: 'gitlab.com' } + end + + it 'stops execution after the first invalid block' do + interpolator.interpolate! + + expect(interpolator).not_to be_valid + expect(interpolator.error_message).to eq('unknown interpolation key: `something`') + end + end + + context 'when there are many invalid arguments' do + let(:header) do + { spec: { inputs: { + allow_failure: { type: 'boolean' }, + image: nil, + parallel: { type: 'number' }, + website: nil + } } } + end + + let(:content) do + "test: 'deploy $[[ inputs.website ]] $[[ inputs.parallel ]] $[[ inputs.allow_failure ]] $[[ inputs.image ]]'" + end + + let(:arguments) do + { allow_failure: 'no', parallel: 'yes', website: 8 } + end + + it 'reports a maximum of 3 errors in the error message' do + interpolator.interpolate! + + expect(interpolator).not_to be_valid + expect(interpolator.error_message).to eq( + '`allow_failure` input: provided value is not a boolean, ' \ + '`image` input: required value has not been provided, ' \ + '`parallel` input: provided value is not a number' + ) + expect(interpolator.errors).to contain_exactly( + '`allow_failure` input: provided value is not a boolean', + '`image` input: required value has not been provided', + '`parallel` input: provided value is not a number', + '`website` input: provided value is not a string' + ) + end + end + + describe '#to_result' do + context 'when interpolation is not used' do + let(:result) do + ::Gitlab::Ci::Config::Yaml::Result.new(config: content) + end + + let(:content) do + "test: 'deploy production'" + end + + let(:arguments) { nil } + + it 'returns original content' do + interpolator.interpolate! + + expect(interpolator.to_result).to eq(content) + end + end + + context 'when interpolation is available' do + let(:header) do + { spec: { inputs: { website: nil } } } + end + + let(:content) do + "test: 'deploy $[[ inputs.website ]]'" + end + + let(:arguments) do + { website: 'gitlab.com' } + end + + it 'correctly interpolates content' do + interpolator.interpolate! + + expect(interpolator.to_result).to eq("test: 'deploy gitlab.com'") + end + end + end +end diff --git a/spec/lib/gitlab/ci/config/interpolation/text_template_spec.rb b/spec/lib/gitlab/ci/config/interpolation/text_template_spec.rb new file mode 100644 index 00000000000..a2f98fc0d5d --- /dev/null +++ b/spec/lib/gitlab/ci/config/interpolation/text_template_spec.rb @@ -0,0 +1,105 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Config::Interpolation::TextTemplate, feature_category: :pipeline_composition do + subject(:template) { described_class.new(config, ctx) } + + let(:config) do + <<~CFG + test: + spec: + env: $[[ inputs.env ]] + + $[[ inputs.key ]]: + name: $[[ inputs.key ]] + script: my-value + parallel: $[[ inputs.parallel ]] + CFG + end + + let(:ctx) do + { inputs: { env: 'dev', key: 'abc', parallel: 6 } } + end + + it 'interpolates the values properly' do + expect(template.interpolated).to eq <<~RESULT + test: + spec: + env: dev + + abc: + name: abc + script: my-value + parallel: 6 + RESULT + end + + context 'when the config has an unknown interpolation key' do + let(:config) { '$[[ xxx.yyy ]]: abc' } + + it 'does not interpolate the config' do + expect(template).not_to be_valid + expect(template.interpolated).to be_nil + expect(template.errors).to contain_exactly('unknown interpolation key: `xxx`') + end + end + + context 'when template consists of nested arrays with hashes and values' do + let(:config) do + <<~CFG + test: + - a-$[[ inputs.key ]]-b + - c-$[[ inputs.key ]]-d: + d-$[[ inputs.key ]]-e + val: 1 + CFG + end + + it 'performs a valid interpolation' do + result = <<~RESULT + test: + - a-abc-b + - c-abc-d: + d-abc-e + val: 1 + RESULT + + expect(template).to be_valid + expect(template.interpolated).to eq result + end + end + + context 'when template contains symbols that need interpolation' do + subject(:template) do + described_class.new("'$[[ inputs.key ]]': 'cde'", ctx) + end + + it 'performs a valid interpolation' do + expect(template).to be_valid + expect(template.interpolated).to eq("'abc': 'cde'") + end + end + + context 'when template is too large' do + before do + stub_application_setting(ci_max_total_yaml_size_bytes: 1) + end + + it 'returns an error' do + expect(template.interpolated).to be_nil + expect(template.errors).to contain_exactly('config too large') + end + end + + context 'when there are too many interpolation blocks' do + before do + stub_const("#{described_class}::MAX_BLOCKS", 1) + end + + it 'returns an error' do + expect(template.interpolated).to be_nil + expect(template.errors).to contain_exactly('too many interpolation blocks') + end + end +end diff --git a/spec/lib/gitlab/ci/config_spec.rb b/spec/lib/gitlab/ci/config_spec.rb index fdf152b3584..76be65d91c4 100644 --- a/spec/lib/gitlab/ci/config_spec.rb +++ b/spec/lib/gitlab/ci/config_spec.rb @@ -43,6 +43,34 @@ RSpec.describe Gitlab::Ci::Config, feature_category: :pipeline_composition do expect(config.to_hash).to eq hash end + context 'when yml has stages' do + let(:yml) do + <<-EOS + image: image:1.0 + stages: + - custom_stage + rspec: + script: + - gem install rspec + - rspec + EOS + end + + specify do + expect(config.to_hash[:stages]).to eq(['.pre', 'custom_stage', '.post']) + end + + context 'with inject_edge_stages option disabled' do + let(:config) do + described_class.new(yml, project: nil, pipeline: nil, sha: nil, user: nil, inject_edge_stages: false) + end + + specify do + expect(config.to_hash[:stages]).to contain_exactly('custom_stage') + end + end + end + describe '#valid?' do it 'is valid' do expect(config).to be_valid diff --git a/spec/lib/gitlab/ci/jwt_v2_spec.rb b/spec/lib/gitlab/ci/jwt_v2_spec.rb index c2ced10620b..1093e6331cd 100644 --- a/spec/lib/gitlab/ci/jwt_v2_spec.rb +++ b/spec/lib/gitlab/ci/jwt_v2_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::JwtV2, feature_category: :continuous_integration do +RSpec.describe Gitlab::Ci::JwtV2, feature_category: :secrets_management do let(:namespace) { build_stubbed(:namespace) } let(:project) { build_stubbed(:project, namespace: namespace) } let(:user) do diff --git a/spec/lib/gitlab/ci/parsers/sbom/cyclonedx_spec.rb b/spec/lib/gitlab/ci/parsers/sbom/cyclonedx_spec.rb index a331af9a9ac..9c8402faf77 100644 --- a/spec/lib/gitlab/ci/parsers/sbom/cyclonedx_spec.rb +++ b/spec/lib/gitlab/ci/parsers/sbom/cyclonedx_spec.rb @@ -33,35 +33,27 @@ RSpec.describe Gitlab::Ci::Parsers::Sbom::Cyclonedx, feature_category: :dependen allow(SecureRandom).to receive(:uuid).and_return(uuid) end - context 'when report JSON is invalid' do - let(:raw_report_data) { '{ ' } + context 'when report is invalid' do + context 'when report JSON is invalid' do + let(:raw_report_data) { '{ ' } - it 'handles errors and adds them to the report' do - expect(report).to receive(:add_error).with(a_string_including("Report JSON is invalid:")) + it 'handles errors and adds them to the report' do + expect(report).to receive(:add_error).with(a_string_including("Report JSON is invalid:")) - expect { parse! }.not_to raise_error + expect { parse! }.not_to raise_error + end end - end - - context 'when report uses an unsupported spec version' do - let(:report_data) { base_report_data.merge({ 'specVersion' => '1.3' }) } - - it 'reports unsupported version as an error' do - expect(report).to receive(:add_error).with("Unsupported CycloneDX spec version. Must be one of: 1.4") - parse! - end - end + context 'when report does not conform to the CycloneDX schema' do + let(:report_valid?) { false } + let(:validator_errors) { %w[error1 error2] } - context 'when report does not conform to the CycloneDX schema' do - let(:report_valid?) { false } - let(:validator_errors) { %w[error1 error2] } + it 'reports all errors returned by the validator' do + expect(report).to receive(:add_error).with("error1") + expect(report).to receive(:add_error).with("error2") - it 'reports all errors returned by the validator' do - expect(report).to receive(:add_error).with("error1") - expect(report).to receive(:add_error).with("error2") - - parse! + parse! + end end end @@ -109,25 +101,26 @@ RSpec.describe Gitlab::Ci::Parsers::Sbom::Cyclonedx, feature_category: :dependen it 'adds each component, ignoring unused attributes' do expect(report).to receive(:add_component) - .with( - an_object_having_attributes( - name: "activesupport", - version: "5.1.4", - component_type: "library", - purl: an_object_having_attributes(type: "gem") - ) - ) + .with( + an_object_having_attributes( + name: "activesupport", + version: "5.1.4", + component_type: "library", + purl: an_object_having_attributes(type: "gem") + ) + ) expect(report).to receive(:add_component) - .with( - an_object_having_attributes( - name: "byebug", - version: "10.0.0", - component_type: "library", - purl: an_object_having_attributes(type: "gem") - ) - ) + .with( + an_object_having_attributes( + name: "byebug", + version: "10.0.0", + component_type: "library", + purl: an_object_having_attributes(type: "gem") + ) + ) expect(report).to receive(:add_component) - .with(an_object_having_attributes(name: "minimal-component", version: nil, component_type: "library")) + .with(an_object_having_attributes(name: "minimal-component", version: nil, + component_type: "library")) parse! end diff --git a/spec/lib/gitlab/ci/parsers/sbom/validators/cyclonedx_schema_validator_spec.rb b/spec/lib/gitlab/ci/parsers/sbom/validators/cyclonedx_schema_validator_spec.rb index acb7c122bcd..9422290761d 100644 --- a/spec/lib/gitlab/ci/parsers/sbom/validators/cyclonedx_schema_validator_spec.rb +++ b/spec/lib/gitlab/ci/parsers/sbom/validators/cyclonedx_schema_validator_spec.rb @@ -4,160 +4,116 @@ require "spec_helper" RSpec.describe Gitlab::Ci::Parsers::Sbom::Validators::CyclonedxSchemaValidator, feature_category: :dependency_management do - # Reports should be valid or invalid according to the specification at - # https://cyclonedx.org/docs/1.4/json/ - - subject(:validator) { described_class.new(report_data) } - - let_it_be(:required_attributes) do + let(:report_data) do { "bomFormat" => "CycloneDX", - "specVersion" => "1.4", + "specVersion" => spec_version, "version" => 1 } end - context "with minimally valid report" do - let_it_be(:report_data) { required_attributes } - - it { is_expected.to be_valid } - end - - context "when report has components" do - let(:report_data) { required_attributes.merge({ "components" => components }) } - - context "with minimally valid components" do - let(:components) do - [ - { - "type" => "library", - "name" => "activesupport" - }, - { - "type" => "library", - "name" => "byebug" - } - ] - end + subject(:validator) { described_class.new(report_data) } - it { is_expected.to be_valid } + shared_examples 'a validator that performs the expected validations' do + let(:required_attributes) do + { + "bomFormat" => "CycloneDX", + "specVersion" => spec_version, + "version" => 1 + } end - context "when components have versions" do - let(:components) do - [ - { - "type" => "library", - "name" => "activesupport", - "version" => "5.1.4" - }, - { - "type" => "library", - "name" => "byebug", - "version" => "10.0.0" - } - ] - end + context "with minimally valid report" do + let(:report_data) { required_attributes } it { is_expected.to be_valid } end - context 'when components have licenses' do - let(:components) do - [ - { - "type" => "library", - "name" => "activesupport", - "version" => "5.1.4", - "licenses" => [ - { "license" => { "id" => "MIT" } } - ] - } - ] - end + context "when report has components" do + let(:report_data) { required_attributes.merge({ "components" => components }) } - it { is_expected.to be_valid } - end - - context 'when components have a signature' do - let(:components) do - [ - { - "type" => "library", - "name" => "activesupport", - "version" => "5.1.4", - "signature" => { - "algorithm" => "ES256", - "publicKey" => { - "kty" => "EC", - "crv" => "P-256", - "x" => "6BKxpty8cI-exDzCkh-goU6dXq3MbcY0cd1LaAxiNrU", - "y" => "mCbcvUzm44j3Lt2b5BPyQloQ91tf2D2V-gzeUxWaUdg" - }, - "value" => "ybT1qz5zHNi4Ndc6y7Zhamuf51IqXkPkZwjH1XcC-KSuBiaQplTw6Jasf2MbCLg3CF7PAdnMO__WSLwvI5r2jA" + context "with minimally valid components" do + let(:components) do + [ + { + "type" => "library", + "name" => "activesupport" + }, + { + "type" => "library", + "name" => "byebug" } - } - ] - end - - it { is_expected.to be_valid } - end + ] + end - context "when components are not valid" do - let(:components) do - [ - { "type" => "foo" }, - { "name" => "activesupport" } - ] + it { is_expected.to be_valid } end - it { is_expected.not_to be_valid } - - it "outputs errors for each validation failure" do - expect(validator.errors).to match_array( + context "when components have versions" do + let(:components) do [ - "property '/components/0' is missing required keys: name", - "property '/components/0/type' is not one of: [\"application\", \"framework\"," \ - " \"library\", \"container\", \"operating-system\", \"device\", \"firmware\", \"file\"]", - "property '/components/1' is missing required keys: type" - ]) - end - end - end - - context "when report has metadata" do - let(:metadata) do - { - "timestamp" => "2022-02-23T08:02:39Z", - "tools" => [{ "vendor" => "GitLab", "name" => "Gemnasium", "version" => "2.34.0" }], - "authors" => [{ "name" => "GitLab", "email" => "support@gitlab.com" }] - } - end + { + "type" => "library", + "name" => "activesupport", + "version" => "5.1.4" + }, + { + "type" => "library", + "name" => "byebug", + "version" => "10.0.0" + } + ] + end - let(:report_data) { required_attributes.merge({ "metadata" => metadata }) } + it { is_expected.to be_valid } + end - it { is_expected.to be_valid } + context 'when components have licenses' do + let(:components) do + [ + { + "type" => "library", + "name" => "activesupport", + "version" => "5.1.4", + "licenses" => [ + { "license" => { "id" => "MIT" } } + ] + } + ] + end - context "when metadata has properties" do - before do - metadata.merge!({ "properties" => properties }) + it { is_expected.to be_valid } end - context "when properties are valid" do - let(:properties) do + context 'when components have a signature' do + let(:components) do [ - { "name" => "gitlab:dependency_scanning:input_file", "value" => "Gemfile.lock" }, - { "name" => "gitlab:dependency_scanning:package_manager", "value" => "bundler" } + { + "type" => "library", + "name" => "activesupport", + "version" => "5.1.4", + "signature" => { + "algorithm" => "ES256", + "publicKey" => { + "kty" => "EC", + "crv" => "P-256", + "x" => "6BKxpty8cI-exDzCkh-goU6dXq3MbcY0cd1LaAxiNrU", + "y" => "mCbcvUzm44j3Lt2b5BPyQloQ91tf2D2V-gzeUxWaUdg" + }, + "value" => "ybT1qz5zHNi4Ndc6y7Zhamuf51IqXkPkZwjH1XcC-KSuBiaQplTw6Jasf2MbCLg3CF7PAdnMO__WSLwvI5r2jA" + } + } ] end it { is_expected.to be_valid } end - context "when properties are invalid" do - let(:properties) do + context "when components are not valid" do + let(:components) do [ - { "name" => ["gitlab:meta:schema_version"], "value" => 1 } + { "type" => "foo" }, + { "name" => "activesupport" } ] end @@ -166,11 +122,75 @@ RSpec.describe Gitlab::Ci::Parsers::Sbom::Validators::CyclonedxSchemaValidator, it "outputs errors for each validation failure" do expect(validator.errors).to match_array( [ - "property '/metadata/properties/0/name' is not of type: string", - "property '/metadata/properties/0/value' is not of type: string" + "property '/components/0' is missing required keys: name", + a_string_starting_with("property '/components/0/type' is not one of:"), + "property '/components/1' is missing required keys: type" ]) end end end + + context "when report has metadata" do + let(:metadata) do + { + "timestamp" => "2022-02-23T08:02:39Z", + "tools" => [{ "vendor" => "GitLab", "name" => "Gemnasium", "version" => "2.34.0" }], + "authors" => [{ "name" => "GitLab", "email" => "support@gitlab.com" }] + } + end + + let(:report_data) { required_attributes.merge({ "metadata" => metadata }) } + + it { is_expected.to be_valid } + + context "when metadata has properties" do + before do + metadata.merge!({ "properties" => properties }) + end + + context "when properties are valid" do + let(:properties) do + [ + { "name" => "gitlab:dependency_scanning:input_file", "value" => "Gemfile.lock" }, + { "name" => "gitlab:dependency_scanning:package_manager", "value" => "bundler" } + ] + end + + it { is_expected.to be_valid } + end + + context "when properties are invalid" do + let(:properties) do + [ + { "name" => ["gitlab:meta:schema_version"], "value" => 1 } + ] + end + + it { is_expected.not_to be_valid } + + it "outputs errors for each validation failure" do + expect(validator.errors).to match_array( + [ + "property '/metadata/properties/0/name' is not of type: string", + "property '/metadata/properties/0/value' is not of type: string" + ]) + end + end + end + end + end + + context 'when spec version is supported' do + where(:spec_version) { %w[1.4 1.5] } + + with_them do + it_behaves_like 'a validator that performs the expected validations' + end + end + + context 'when spec version is not supported' do + let(:spec_version) { '1.3' } + + it { is_expected.not_to be_valid } end end diff --git a/spec/lib/gitlab/ci/pipeline/chain/cancel_pending_pipelines_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/cancel_pending_pipelines_spec.rb index 31bffcbeb2a..00f834fcf80 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/cancel_pending_pipelines_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/cancel_pending_pipelines_spec.rb @@ -5,7 +5,7 @@ require 'spec_helper' RSpec.describe Gitlab::Ci::Pipeline::Chain::CancelPendingPipelines, feature_category: :continuous_integration do let_it_be(:project) { create(:project) } let_it_be(:user) { create(:user) } - let_it_be(:pipeline) { create(:ci_pipeline, project: project) } + let_it_be_with_reload(:pipeline) { create(:ci_pipeline, project: project) } let_it_be(:command) { Gitlab::Ci::Pipeline::Chain::Command.new(project: project, current_user: user) } let_it_be(:step) { described_class.new(pipeline, command) } @@ -17,5 +17,18 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::CancelPendingPipelines, feature_cate subject end + + context 'with scheduled pipelines' do + before do + pipeline.source = :schedule + end + + it 'enqueues LowUrgencyCancelRedundantPipelinesWorker' do + expect(Ci::LowUrgencyCancelRedundantPipelinesWorker) + .to receive(:perform_async).with(pipeline.id) + + subject + end + end end end diff --git a/spec/lib/gitlab/ci/pipeline/chain/evaluate_workflow_rules_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/evaluate_workflow_rules_spec.rb index eb5a37f19f4..44ccb1eeae1 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/evaluate_workflow_rules_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/evaluate_workflow_rules_spec.rb @@ -12,10 +12,13 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::EvaluateWorkflowRules do end let(:step) { described_class.new(pipeline, command) } + let(:ff_always_set_pipeline_failure_reason) { true } describe '#perform!' do context 'when pipeline has been skipped by workflow configuration' do before do + stub_feature_flags(always_set_pipeline_failure_reason: ff_always_set_pipeline_failure_reason) + allow(step).to receive(:workflow_rules_result) .and_return( double(pass?: false, variables: {}) @@ -39,6 +42,20 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::EvaluateWorkflowRules do it 'saves workflow_rules_result' do expect(command.workflow_rules_result.variables).to eq({}) end + + it 'sets the failure reason', :aggregate_failures do + expect(pipeline).to be_failed + expect(pipeline).to be_filtered_by_workflow_rules + end + + context 'when always_set_pipeline_failure_reason is disabled' do + let(:ff_always_set_pipeline_failure_reason) { false } + + it 'does not set the failure reason', :aggregate_failures do + expect(pipeline).not_to be_failed + expect(pipeline.failure_reason).to be_blank + end + end end context 'when pipeline has not been skipped by workflow configuration' do @@ -67,6 +84,10 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::EvaluateWorkflowRules do it 'saves workflow_rules_result' do expect(command.workflow_rules_result.variables).to eq({ 'VAR1' => 'val2', 'VAR2' => 3 }) end + + it 'does not set a failure reason' do + expect(pipeline).not_to be_filtered_by_workflow_rules + end end end end diff --git a/spec/lib/gitlab/ci/pipeline/chain/helpers_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/helpers_spec.rb index 96ada90b4e1..84c2fb6525e 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/helpers_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/helpers_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::Pipeline::Chain::Helpers do +RSpec.describe Gitlab::Ci::Pipeline::Chain::Helpers, feature_category: :continuous_integration do let(:helper_class) do Class.new do include Gitlab::Ci::Pipeline::Chain::Helpers @@ -38,14 +38,35 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Helpers do describe '.error' do shared_examples 'error function' do specify do - expect(pipeline).to receive(:drop!).with(drop_reason).and_call_original expect(pipeline).to receive(:add_error_message).with(message).and_call_original - expect(pipeline).to receive(:ensure_project_iid!).twice.and_call_original + + if command.save_incompleted + expect(pipeline).to receive(:ensure_project_iid!).twice.and_call_original + expect(pipeline).to receive(:drop!).with(drop_reason).and_call_original + end subject.error(message, config_error: config_error, drop_reason: drop_reason) expect(pipeline.yaml_errors).to eq(yaml_error) expect(pipeline.errors[:base]).to include(message) + expect(pipeline.status).to eq 'failed' + expect(pipeline.failure_reason).to eq drop_reason.to_s + end + + context 'when feature flag always_set_pipeline_failure_reason is false' do + before do + stub_feature_flags(always_set_pipeline_failure_reason: false) + end + + specify do + subject.error(message, config_error: config_error, drop_reason: drop_reason) + + if command.save_incompleted + expect(pipeline.failure_reason).to eq drop_reason.to_s + else + expect(pipeline.failure_reason).not_to be_present + end + end end end @@ -79,6 +100,43 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Helpers do let(:yaml_error) { nil } it_behaves_like "error function" + + specify do + subject.error(message, config_error: config_error, drop_reason: drop_reason) + + expect(pipeline).to be_persisted + end + + context ' when the drop reason is not persistable' do + let(:drop_reason) { :filtered_by_rules } + let(:command) { double(project: nil) } + + specify do + expect(command).to receive(:increment_pipeline_failure_reason_counter) + + subject.error(message, config_error: config_error, drop_reason: drop_reason) + + expect(pipeline).to be_failed + expect(pipeline.failure_reason).to eq drop_reason.to_s + expect(pipeline).not_to be_persisted + end + end + + context 'when save_incompleted is false' do + let(:command) { double(save_incompleted: false, project: nil) } + + before do + allow(command).to receive(:increment_pipeline_failure_reason_counter) + end + + it_behaves_like "error function" + + specify do + subject.error(message, config_error: config_error, drop_reason: drop_reason) + + expect(pipeline).not_to be_persisted + end + end end end end diff --git a/spec/lib/gitlab/ci/pipeline/chain/populate_metadata_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/populate_metadata_spec.rb index 00200b57b1e..732748d8c8b 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/populate_metadata_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/populate_metadata_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::Pipeline::Chain::PopulateMetadata do +RSpec.describe Gitlab::Ci::Pipeline::Chain::PopulateMetadata, feature_category: :pipeline_composition do let_it_be(:project) { create(:project, :repository) } let_it_be(:user) { create(:user) } @@ -43,16 +43,28 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::PopulateMetadata do stub_ci_pipeline_yaml_file(YAML.dump(config)) end - context 'with pipeline name' do - let(:config) do - { workflow: { name: ' Pipeline name ' }, rspec: { script: 'rspec' } } - end - + shared_examples 'not breaking the chain' do it 'does not break the chain' do run_chain expect(step.break?).to be false end + end + + shared_examples 'not saving pipeline metadata' do + it 'does not save pipeline metadata' do + run_chain + + expect(pipeline.pipeline_metadata).to be_nil + end + end + + context 'with pipeline name' do + let(:config) do + { workflow: { name: ' Pipeline name ' }, rspec: { script: 'rspec' } } + end + + it_behaves_like 'not breaking the chain' it 'builds pipeline_metadata' do run_chain @@ -67,22 +79,14 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::PopulateMetadata do { workflow: { name: ' ' }, rspec: { script: 'rspec' } } end - it 'strips whitespace from name' do - run_chain - - expect(pipeline.pipeline_metadata).to be_nil - end + it_behaves_like 'not saving pipeline metadata' context 'with empty name after variable substitution' do let(:config) do { workflow: { name: '$VAR1' }, rspec: { script: 'rspec' } } end - it 'does not save empty name' do - run_chain - - expect(pipeline.pipeline_metadata).to be_nil - end + it_behaves_like 'not saving pipeline metadata' end end @@ -127,4 +131,140 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::PopulateMetadata do end end end + + context 'with auto_cancel' do + let(:on_new_commit) { 'interruptible' } + let(:on_job_failure) { 'all' } + let(:auto_cancel) { { on_new_commit: on_new_commit, on_job_failure: on_job_failure } } + let(:config) { { workflow: { auto_cancel: auto_cancel }, rspec: { script: 'rspec' } } } + + it_behaves_like 'not breaking the chain' + + it 'builds pipeline_metadata' do + run_chain + + expect(pipeline.pipeline_metadata.auto_cancel_on_new_commit).to eq('interruptible') + expect(pipeline.pipeline_metadata.auto_cancel_on_job_failure).to eq('all') + expect(pipeline.pipeline_metadata).not_to be_persisted + end + + context 'with no auto_cancel' do + let(:config) do + { rspec: { script: 'rspec' } } + end + + it_behaves_like 'not saving pipeline metadata' + end + + context 'with auto_cancel: nil' do + let(:auto_cancel) { nil } + + it_behaves_like 'not saving pipeline metadata' + end + + context 'with auto_cancel_on_new_commit and no auto_cancel_on_job_failure' do + let(:auto_cancel) { { on_new_commit: on_new_commit } } + + it 'builds pipeline_metadata' do + run_chain + + expect(pipeline.pipeline_metadata.auto_cancel_on_new_commit).to eq('interruptible') + expect(pipeline.pipeline_metadata.auto_cancel_on_job_failure).to eq('none') + expect(pipeline.pipeline_metadata).not_to be_persisted + end + end + + context 'with auto_cancel_on_job_failure and no auto_cancel_on_new_commit' do + let(:auto_cancel) { { on_job_failure: on_job_failure } } + + it 'builds pipeline_metadata' do + run_chain + + expect(pipeline.pipeline_metadata.auto_cancel_on_new_commit).to eq('conservative') + expect(pipeline.pipeline_metadata.auto_cancel_on_job_failure).to eq('all') + expect(pipeline.pipeline_metadata).not_to be_persisted + end + end + + context 'with auto_cancel_on_new_commit: nil and auto_cancel_on_job_failure: nil' do + let(:on_new_commit) { nil } + let(:on_job_failure) { nil } + + it_behaves_like 'not saving pipeline metadata' + end + + context 'with auto_cancel_on_new_commit valid and auto_cancel_on_job_failure: nil' do + let(:on_job_failure) { nil } + + it 'builds pipeline_metadata' do + run_chain + + expect(pipeline.pipeline_metadata.auto_cancel_on_new_commit).to eq('interruptible') + expect(pipeline.pipeline_metadata.auto_cancel_on_job_failure).to eq('none') + expect(pipeline.pipeline_metadata).not_to be_persisted + end + end + + context 'with auto_cancel_on_new_commit: nil and auto_cancel_on_job_failure valid' do + let(:on_new_commit) { nil } + + it 'builds pipeline_metadata' do + run_chain + + expect(pipeline.pipeline_metadata.auto_cancel_on_new_commit).to eq('conservative') + expect(pipeline.pipeline_metadata.auto_cancel_on_job_failure).to eq('all') + expect(pipeline.pipeline_metadata).not_to be_persisted + end + end + + context 'when auto_cancel_on_job_failure: none' do + let(:on_job_failure) { 'none' } + + it 'builds pipeline_metadata' do + run_chain + + expect(pipeline.pipeline_metadata.auto_cancel_on_job_failure).to eq('none') + expect(pipeline.pipeline_metadata).not_to be_persisted + end + end + + context 'when auto_cancel_pipeline_on_job_failure feature is disabled' do + before do + stub_feature_flags(auto_cancel_pipeline_on_job_failure: false) + end + + it 'ignores the auto_cancel_on_job_failure value' do + run_chain + + expect(pipeline.pipeline_metadata.auto_cancel_on_job_failure).to eq('none') + expect(pipeline.pipeline_metadata).not_to be_persisted + end + end + end + + context 'with both pipeline name and auto_cancel' do + let(:config) do + { + workflow: { + name: 'Pipeline name', + auto_cancel: { + on_new_commit: 'interruptible', + on_job_failure: 'none' + } + }, + rspec: { script: 'rspec' } + } + end + + it_behaves_like 'not breaking the chain' + + it 'builds pipeline_metadata' do + run_chain + + expect(pipeline.pipeline_metadata.name).to eq('Pipeline name') + expect(pipeline.pipeline_metadata.auto_cancel_on_new_commit).to eq('interruptible') + expect(pipeline.pipeline_metadata.auto_cancel_on_job_failure).to eq('none') + expect(pipeline.pipeline_metadata).not_to be_persisted + end + end end diff --git a/spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb index 91bb94bbb11..476b1be35a9 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb @@ -34,12 +34,15 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Populate, feature_category: :continu { rspec: { script: 'rspec' } } end + let(:ff_always_set_pipeline_failure_reason) { true } + def run_chain dependencies.map(&:perform!) step.perform! end before do + stub_feature_flags(always_set_pipeline_failure_reason: ff_always_set_pipeline_failure_reason) stub_ci_pipeline_yaml_file(YAML.dump(config)) end @@ -100,7 +103,27 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Populate, feature_category: :continu it 'increments the error metric' do counter = Gitlab::Metrics.counter(:gitlab_ci_pipeline_failure_reasons, 'desc') - expect { run_chain }.to change { counter.get(reason: 'unknown_failure') }.by(1) + expect { run_chain }.to change { counter.get(reason: 'filtered_by_rules') }.by(1) + end + + it 'sets the failure reason without persisting the pipeline', :aggregate_failures do + run_chain + + expect(pipeline).not_to be_persisted + expect(pipeline).to be_failed + expect(pipeline).to be_filtered_by_rules + end + + context 'when ff always_set_pipeline_failure_reason is disabled' do + let(:ff_always_set_pipeline_failure_reason) { false } + + it 'sets the failure reason without persisting the pipeline', :aggregate_failures do + run_chain + + expect(pipeline).not_to be_persisted + expect(pipeline).not_to be_failed + expect(pipeline).not_to be_filtered_by_rules + end end end diff --git a/spec/lib/gitlab/ci/pipeline/chain/validate/external_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/validate/external_spec.rb index 52a00e0d501..4017076d29f 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/validate/external_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/validate/external_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::Pipeline::Chain::Validate::External do +RSpec.describe Gitlab::Ci::Pipeline::Chain::Validate::External, feature_category: :continuous_integration do let_it_be(:project) { create(:project) } let_it_be(:user) { create(:user, :with_sign_ins) } @@ -328,11 +328,12 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Validate::External do context 'when save_incompleted is false' do let(:save_incompleted) { false } - it 'adds errors to the pipeline without dropping it' do + it 'adds errors to the pipeline without persisting it', :aggregate_failures do perform! - expect(pipeline.status).to eq('pending') expect(pipeline).not_to be_persisted + expect(pipeline.status).to eq('failed') + expect(pipeline).to be_external_validation_failure expect(pipeline.errors.to_a).to include('External validation failed') end diff --git a/spec/lib/gitlab/ci/reports/sbom/source_spec.rb b/spec/lib/gitlab/ci/reports/sbom/source_spec.rb index c1eaea511b7..09a601833ad 100644 --- a/spec/lib/gitlab/ci/reports/sbom/source_spec.rb +++ b/spec/lib/gitlab/ci/reports/sbom/source_spec.rb @@ -5,47 +5,93 @@ require 'fast_spec_helper' RSpec.describe Gitlab::Ci::Reports::Sbom::Source, feature_category: :dependency_management do let(:attributes) do { - type: :dependency_scanning, - data: { - 'category' => 'development', - 'input_file' => { 'path' => 'package-lock.json' }, - 'source_file' => { 'path' => 'package.json' }, - 'package_manager' => { 'name' => 'npm' }, - 'language' => { 'name' => 'JavaScript' } - } + type: type, + data: { 'category' => 'development', + 'package_manager' => { 'name' => 'npm' }, + 'language' => { 'name' => 'JavaScript' } }.merge(extra_attributes) } end - subject { described_class.new(**attributes) } + subject(:source) { described_class.new(**attributes) } - it 'has correct attributes' do - expect(subject).to have_attributes( - source_type: attributes[:type], - data: attributes[:data] - ) - end + shared_examples_for 'it has correct common attributes' do + it 'has correct type and data' do + expect(subject).to have_attributes( + source_type: type, + data: attributes[:data] + ) + end - describe '#source_file_path' do - it 'returns the correct source_file_path' do - expect(subject.source_file_path).to eq('package.json') + describe '#packager' do + it 'returns the correct package manager name' do + expect(subject.packager).to eq("npm") + end end - end - describe '#input_file_path' do - it 'returns the correct input_file_path' do - expect(subject.input_file_path).to eq("package-lock.json") + describe '#language' do + it 'returns the correct language' do + expect(subject.language).to eq("JavaScript") + end end end - describe '#packager' do - it 'returns the correct package manager name' do - expect(subject.packager).to eq("npm") + context 'when dependency scanning' do + let(:type) { :dependency_scanning } + let(:extra_attributes) do + { + 'input_file' => { 'path' => 'package-lock.json' }, + 'source_file' => { 'path' => 'package.json' } + } + end + + it_behaves_like 'it has correct common attributes' + + describe '#source_file_path' do + it 'returns the correct source_file_path' do + expect(subject.source_file_path).to eq('package.json') + end + end + + describe '#input_file_path' do + it 'returns the correct input_file_path' do + expect(subject.input_file_path).to eq("package-lock.json") + end end end - describe '#language' do - it 'returns the correct langauge' do - expect(subject.language).to eq("JavaScript") + context 'when container scanning' do + let(:type) { :container_scanning } + let(:extra_attributes) do + { + "image" => { "name" => "rhel", "tag" => "7.1" }, + "operating_system" => { "name" => "Red Hat Enterprise Linux", "version" => "7" } + } + end + + it_behaves_like 'it has correct common attributes' + + describe "#image_name" do + subject { source.image_name } + + it { is_expected.to eq("rhel") } + end + + describe "#image_tag" do + subject { source.image_tag } + + it { is_expected.to eq("7.1") } + end + + describe "#operating_system_name" do + subject { source.operating_system_name } + + it { is_expected.to eq("Red Hat Enterprise Linux") } + end + + describe "#operating_system_version" do + subject { source.operating_system_version } + + it { is_expected.to eq("7") } end end end diff --git a/spec/lib/gitlab/ci/runner_instructions_spec.rb b/spec/lib/gitlab/ci/runner_instructions_spec.rb index 31c53d4a030..6da649393f3 100644 --- a/spec/lib/gitlab/ci/runner_instructions_spec.rb +++ b/spec/lib/gitlab/ci/runner_instructions_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::RunnerInstructions, feature_category: :runner_fleet do +RSpec.describe Gitlab::Ci::RunnerInstructions, feature_category: :fleet_visibility do using RSpec::Parameterized::TableSyntax let(:params) { {} } diff --git a/spec/lib/gitlab/ci/runner_releases_spec.rb b/spec/lib/gitlab/ci/runner_releases_spec.rb index 9e211327dee..126a5b85471 100644 --- a/spec/lib/gitlab/ci/runner_releases_spec.rb +++ b/spec/lib/gitlab/ci/runner_releases_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::RunnerReleases, feature_category: :runner_fleet do +RSpec.describe Gitlab::Ci::RunnerReleases, feature_category: :fleet_visibility do subject { described_class.instance } let(:runner_releases_url) { 'http://testurl.com/runner_public_releases' } diff --git a/spec/lib/gitlab/ci/runner_upgrade_check_spec.rb b/spec/lib/gitlab/ci/runner_upgrade_check_spec.rb index 526d6cba657..778c0aa69de 100644 --- a/spec/lib/gitlab/ci/runner_upgrade_check_spec.rb +++ b/spec/lib/gitlab/ci/runner_upgrade_check_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::RunnerUpgradeCheck, feature_category: :runner_fleet do +RSpec.describe Gitlab::Ci::RunnerUpgradeCheck, feature_category: :fleet_visibility do using RSpec::Parameterized::TableSyntax subject(:instance) { described_class.new(gitlab_version, runner_releases) } diff --git a/spec/lib/gitlab/ci/templates/Diffblue_Cover_spec.rb b/spec/lib/gitlab/ci/templates/Diffblue_Cover_spec.rb new file mode 100644 index 00000000000..c16356bfda7 --- /dev/null +++ b/spec/lib/gitlab/ci/templates/Diffblue_Cover_spec.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Diffblue-Cover.gitlab-ci.yml', feature_category: :continuous_integration do + subject(:template) { Gitlab::Template::GitlabCiYmlTemplate.find('Diffblue-Cover') } + + describe 'the created pipeline' do + let(:pipeline_branch) { 'patch-1' } + let_it_be(:project) { create(:project, :repository, create_branch: 'patch-1') } + let(:user) { project.first_owner } + + let(:mr_service) { MergeRequests::CreatePipelineService.new(project: project, current_user: user) } + let(:merge_request) { create(:merge_request, :simple, source_project: project, source_branch: pipeline_branch) } + let(:mr_pipeline) { mr_service.execute(merge_request).payload } + let(:mr_build_names) { mr_pipeline.builds.pluck(:name) } + + before do + stub_ci_pipeline_yaml_file(template.content) + end + + it 'creates diffblue-cover jobs' do + expect(mr_build_names).to include('diffblue-cover') + end + end +end diff --git a/spec/lib/gitlab/ci/templates/templates_spec.rb b/spec/lib/gitlab/ci/templates/templates_spec.rb index 36c6e805bdf..98f0d32960b 100644 --- a/spec/lib/gitlab/ci/templates/templates_spec.rb +++ b/spec/lib/gitlab/ci/templates/templates_spec.rb @@ -20,6 +20,7 @@ RSpec.describe 'CI YML Templates' do context 'that support autodevops' do exceptions = [ + 'Diffblue-Cover.gitlab-ci.yml', # no auto-devops 'Security/DAST.gitlab-ci.yml', # DAST stage is defined inside AutoDevops yml 'Security/DAST-API.gitlab-ci.yml', # no auto-devops 'Security/API-Fuzzing.gitlab-ci.yml', # no auto-devops diff --git a/spec/lib/gitlab/ci/variables/builder/pipeline_spec.rb b/spec/lib/gitlab/ci/variables/builder/pipeline_spec.rb index 860a1fd30bd..f8d67a6f0b4 100644 --- a/spec/lib/gitlab/ci/variables/builder/pipeline_spec.rb +++ b/spec/lib/gitlab/ci/variables/builder/pipeline_spec.rb @@ -66,6 +66,7 @@ RSpec.describe Gitlab::Ci::Variables::Builder::Pipeline, feature_category: :secr let_it_be(:assignees) { create_list(:user, 2) } let_it_be(:milestone) { create(:milestone, project: project) } let_it_be(:labels) { create_list(:label, 2) } + let(:merge_request_description) { nil } let(:merge_request) do create(:merge_request, :simple, @@ -73,6 +74,7 @@ RSpec.describe Gitlab::Ci::Variables::Builder::Pipeline, feature_category: :secr target_project: project, assignees: assignees, milestone: milestone, + description: merge_request_description, labels: labels) end @@ -113,6 +115,8 @@ RSpec.describe Gitlab::Ci::Variables::Builder::Pipeline, feature_category: :secr merge_request.source_branch ).to_s, 'CI_MERGE_REQUEST_TITLE' => merge_request.title, + 'CI_MERGE_REQUEST_DESCRIPTION' => merge_request.description, + 'CI_MERGE_REQUEST_DESCRIPTION_IS_TRUNCATED' => 'false', 'CI_MERGE_REQUEST_ASSIGNEES' => merge_request.assignee_username_list, 'CI_MERGE_REQUEST_MILESTONE' => milestone.title, 'CI_MERGE_REQUEST_LABELS' => labels.map(&:title).sort.join(','), @@ -121,6 +125,78 @@ RSpec.describe Gitlab::Ci::Variables::Builder::Pipeline, feature_category: :secr 'CI_MERGE_REQUEST_SQUASH_ON_MERGE' => merge_request.squash_on_merge?.to_s end + context 'when merge request description hits the limit' do + let(:merge_request_description) { 'a' * (MergeRequest::CI_MERGE_REQUEST_DESCRIPTION_MAX_LENGTH + 1) } + + it 'truncates the exposed description' do + truncated_description = merge_request.description.truncate( + MergeRequest::CI_MERGE_REQUEST_DESCRIPTION_MAX_LENGTH + ) + expect(subject.to_hash) + .to include( + 'CI_MERGE_REQUEST_DESCRIPTION' => truncated_description, + 'CI_MERGE_REQUEST_DESCRIPTION_IS_TRUNCATED' => 'true' + ) + end + end + + context 'when merge request description fits the length limit' do + let(:merge_request_description) { 'a' * (MergeRequest::CI_MERGE_REQUEST_DESCRIPTION_MAX_LENGTH - 1) } + + it 'does not truncate the exposed description' do + expect(subject.to_hash) + .to include( + 'CI_MERGE_REQUEST_DESCRIPTION' => merge_request.description, + 'CI_MERGE_REQUEST_DESCRIPTION_IS_TRUNCATED' => 'false' + ) + end + end + + context 'when truncate_ci_merge_request_description feature flag is disabled' do + before do + stub_feature_flags(truncate_ci_merge_request_description: false) + end + + context 'when merge request description hits the limit' do + let(:merge_request_description) { 'a' * (MergeRequest::CI_MERGE_REQUEST_DESCRIPTION_MAX_LENGTH + 1) } + + it 'does not truncate the exposed description' do + expect(subject.to_hash) + .to include( + 'CI_MERGE_REQUEST_DESCRIPTION' => merge_request.description + ) + expect(subject.to_hash) + .not_to have_key('CI_MERGE_REQUEST_DESCRIPTION_IS_TRUNCATED') + end + end + + context 'when merge request description fits the length limit' do + let(:merge_request_description) { 'a' * (MergeRequest::CI_MERGE_REQUEST_DESCRIPTION_MAX_LENGTH - 1) } + + it 'does not truncate the exposed description' do + expect(subject.to_hash) + .to include( + 'CI_MERGE_REQUEST_DESCRIPTION' => merge_request.description + ) + expect(subject.to_hash) + .not_to have_key('CI_MERGE_REQUEST_DESCRIPTION_IS_TRUNCATED') + end + end + + context 'when merge request description does not exist' do + let(:merge_request_description) { nil } + + it 'does not truncate the exposed description' do + expect(subject.to_hash) + .to include( + 'CI_MERGE_REQUEST_DESCRIPTION' => merge_request.description + ) + expect(subject.to_hash) + .not_to have_key('CI_MERGE_REQUEST_DESCRIPTION_IS_TRUNCATED') + end + end + end + it 'exposes diff variables' do expect(subject.to_hash) .to include( @@ -214,6 +290,7 @@ RSpec.describe Gitlab::Ci::Variables::Builder::Pipeline, feature_category: :secr 'CI_MERGE_REQUEST_SOURCE_BRANCH_NAME' => merge_request.source_branch.to_s, 'CI_MERGE_REQUEST_SOURCE_BRANCH_SHA' => merge_request.source_branch_sha, 'CI_MERGE_REQUEST_TITLE' => merge_request.title, + 'CI_MERGE_REQUEST_DESCRIPTION' => merge_request.description, 'CI_MERGE_REQUEST_ASSIGNEES' => merge_request.assignee_username_list, 'CI_MERGE_REQUEST_MILESTONE' => milestone.title, 'CI_MERGE_REQUEST_LABELS' => labels.map(&:title).sort.join(','), diff --git a/spec/lib/gitlab/ci/variables/downstream/generator_spec.rb b/spec/lib/gitlab/ci/variables/downstream/generator_spec.rb index cd68b0cdf2b..f5845e492bc 100644 --- a/spec/lib/gitlab/ci/variables/downstream/generator_spec.rb +++ b/spec/lib/gitlab/ci/variables/downstream/generator_spec.rb @@ -39,6 +39,15 @@ RSpec.describe Gitlab::Ci::Variables::Downstream::Generator, feature_category: : ] end + let(:pipeline_dotenv_variables) do + [ + { key: 'PIPELINE_DOTENV_VAR1', value: 'variable 1' }, + { key: 'PIPELINE_DOTENV_VAR2', value: 'variable 2' }, + { key: 'PIPELINE_DOTENV_RAW_VAR3', value: '$REF1', raw: true }, + { key: 'PIPELINE_DOTENV_INTERPOLATION_VAR4', value: 'interpolate $REF1 $REF2' } + ] + end + let(:bridge) do instance_double( 'Ci::Bridge', @@ -48,7 +57,8 @@ RSpec.describe Gitlab::Ci::Variables::Downstream::Generator, feature_category: : expand_file_refs?: false, yaml_variables: yaml_variables, pipeline_variables: pipeline_variables, - pipeline_schedule_variables: pipeline_schedule_variables + pipeline_schedule_variables: pipeline_schedule_variables, + dependency_variables: pipeline_dotenv_variables ) end @@ -69,7 +79,12 @@ RSpec.describe Gitlab::Ci::Variables::Downstream::Generator, feature_category: : { key: 'PIPELINE_SCHEDULE_VAR1', value: 'variable 1' }, { key: 'PIPELINE_SCHEDULE_VAR2', value: 'variable 2' }, { key: 'PIPELINE_SCHEDULE_RAW_VAR3', value: '$REF1', raw: true }, - { key: 'PIPELINE_SCHEDULE_INTERPOLATION_VAR4', value: 'interpolate ref 1 ref 2' } + { key: 'PIPELINE_SCHEDULE_INTERPOLATION_VAR4', value: 'interpolate ref 1 ref 2' }, + { key: 'PIPELINE_DOTENV_VAR1', value: 'variable 1' }, + { key: 'PIPELINE_DOTENV_VAR2', value: 'variable 2' }, + { key: 'PIPELINE_DOTENV_RAW_VAR3', value: '$REF1', raw: true }, + { key: 'PIPELINE_DOTENV_INTERPOLATION_VAR4', value: 'interpolate ref 1 ref 2' } + ] expect(generator.calculate).to contain_exactly(*expected) @@ -79,6 +94,7 @@ RSpec.describe Gitlab::Ci::Variables::Downstream::Generator, feature_category: : allow(bridge).to receive(:yaml_variables).and_return([]) allow(bridge).to receive(:pipeline_variables).and_return([]) allow(bridge).to receive(:pipeline_schedule_variables).and_return([]) + allow(bridge).to receive(:dependency_variables).and_return([]) expect(generator.calculate).to be_empty end @@ -105,6 +121,10 @@ RSpec.describe Gitlab::Ci::Variables::Downstream::Generator, feature_category: : [{ key: 'PIPELINE_SCHEDULE_INTERPOLATION_VAR', value: 'interpolate $REF1 $REF2 $FILE_REF3 $FILE_REF4' }] end + let(:pipeline_dotenv_variables) do + [{ key: 'PIPELINE_DOTENV_INTERPOLATION_VAR', value: 'interpolate $REF1 $REF2 $FILE_REF3 $FILE_REF4' }] + end + context 'when expand_file_refs is true' do before do allow(bridge).to receive(:expand_file_refs?).and_return(true) @@ -114,7 +134,8 @@ RSpec.describe Gitlab::Ci::Variables::Downstream::Generator, feature_category: : expected = [ { key: 'INTERPOLATION_VAR', value: 'interpolate ref 1 ref 3 ' }, { key: 'PIPELINE_INTERPOLATION_VAR', value: 'interpolate ref 1 ref 3 ' }, - { key: 'PIPELINE_SCHEDULE_INTERPOLATION_VAR', value: 'interpolate ref 1 ref 3 ' } + { key: 'PIPELINE_SCHEDULE_INTERPOLATION_VAR', value: 'interpolate ref 1 ref 3 ' }, + { key: 'PIPELINE_DOTENV_INTERPOLATION_VAR', value: 'interpolate ref 1 ref 3 ' } ] expect(generator.calculate).to contain_exactly(*expected) @@ -131,6 +152,7 @@ RSpec.describe Gitlab::Ci::Variables::Downstream::Generator, feature_category: : { key: 'INTERPOLATION_VAR', value: 'interpolate ref 1 $FILE_REF3 ' }, { key: 'PIPELINE_INTERPOLATION_VAR', value: 'interpolate ref 1 $FILE_REF3 ' }, { key: 'PIPELINE_SCHEDULE_INTERPOLATION_VAR', value: 'interpolate ref 1 $FILE_REF3 ' }, + { key: 'PIPELINE_DOTENV_INTERPOLATION_VAR', value: 'interpolate ref 1 $FILE_REF3 ' }, { key: 'FILE_REF3', value: 'ref 3', variable_type: :file } ] diff --git a/spec/lib/gitlab/ci/yaml_processor/test_cases/interruptible_spec.rb b/spec/lib/gitlab/ci/yaml_processor/test_cases/interruptible_spec.rb new file mode 100644 index 00000000000..03ff7077969 --- /dev/null +++ b/spec/lib/gitlab/ci/yaml_processor/test_cases/interruptible_spec.rb @@ -0,0 +1,96 @@ +# frozen_string_literal: true + +require 'spec_helper' + +module Gitlab + module Ci + RSpec.describe YamlProcessor, feature_category: :pipeline_composition do + subject(:processor) { described_class.new(config, user: nil).execute } + + let(:builds) { processor.builds } + + context 'with interruptible' do + let(:default_config) { nil } + + let(:config) do + <<~YAML + #{default_config} + + build1: + script: rspec + interruptible: true + + build2: + script: rspec + interruptible: false + + build3: + script: rspec + + bridge1: + trigger: some/project + interruptible: true + + bridge2: + trigger: some/project + interruptible: false + + bridge3: + trigger: some/project + YAML + end + + it 'returns jobs with their interruptible value' do + expect(builds).to contain_exactly( + a_hash_including(name: 'build1', interruptible: true), + a_hash_including(name: 'build2', interruptible: false), + a_hash_including(name: 'build3').and(exclude(:interruptible)), + a_hash_including(name: 'bridge1', interruptible: true), + a_hash_including(name: 'bridge2', interruptible: false), + a_hash_including(name: 'bridge3').and(exclude(:interruptible)) + ) + end + + context 'when default:interruptible is true' do + let(:default_config) do + <<~YAML + default: + interruptible: true + YAML + end + + it 'returns jobs with their interruptible value' do + expect(builds).to contain_exactly( + a_hash_including(name: 'build1', interruptible: true), + a_hash_including(name: 'build2', interruptible: false), + a_hash_including(name: 'build3', interruptible: true), + a_hash_including(name: 'bridge1', interruptible: true), + a_hash_including(name: 'bridge2', interruptible: false), + a_hash_including(name: 'bridge3', interruptible: true) + ) + end + end + + context 'when default:interruptible is false' do + let(:default_config) do + <<~YAML + default: + interruptible: false + YAML + end + + it 'returns jobs with their interruptible value' do + expect(builds).to contain_exactly( + a_hash_including(name: 'build1', interruptible: true), + a_hash_including(name: 'build2', interruptible: false), + a_hash_including(name: 'build3', interruptible: false), + a_hash_including(name: 'bridge1', interruptible: true), + a_hash_including(name: 'bridge2', interruptible: false), + a_hash_including(name: 'bridge3', interruptible: false) + ) + end + end + end + end + end +end diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb index f01c1c7d053..844a6849c8f 100644 --- a/spec/lib/gitlab/ci/yaml_processor_spec.rb +++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb @@ -123,55 +123,6 @@ module Gitlab end end - describe 'interruptible entry' do - describe 'interruptible job' do - let(:config) do - YAML.dump(rspec: { script: 'rspec', interruptible: true }) - end - - it { expect(rspec_build[:interruptible]).to be_truthy } - end - - describe 'interruptible job with default value' do - let(:config) do - YAML.dump(rspec: { script: 'rspec' }) - end - - it { expect(rspec_build).not_to have_key(:interruptible) } - end - - describe 'uninterruptible job' do - let(:config) do - YAML.dump(rspec: { script: 'rspec', interruptible: false }) - end - - it { expect(rspec_build[:interruptible]).to be_falsy } - end - - it "returns interruptible when overridden for job" do - config = YAML.dump({ default: { interruptible: true }, - rspec: { script: "rspec" } }) - - config_processor = described_class.new(config).execute - builds = config_processor.builds.select { |b| b[:stage] == "test" } - - expect(builds.size).to eq(1) - expect(builds.first).to eq({ - stage: "test", - stage_idx: 2, - name: "rspec", - only: { refs: %w[branches tags] }, - options: { script: ["rspec"] }, - interruptible: true, - allow_failure: false, - when: "on_success", - job_variables: [], - root_variables_inheritance: true, - scheduling_type: :stage - }) - end - end - describe 'retry entry' do context 'when retry count is specified' do let(:config) do @@ -544,6 +495,27 @@ module Gitlab expect(subject.workflow_name).to be_nil end end + + context 'with auto_cancel' do + let(:config) do + <<-YML + workflow: + auto_cancel: + on_new_commit: interruptible + on_job_failure: all + + hello: + script: echo world + YML + end + + it 'parses the workflow:auto_cancel as workflow_auto_cancel' do + expect(subject.workflow_auto_cancel).to eq({ + on_new_commit: 'interruptible', + on_job_failure: 'all' + }) + end + end end describe '#warnings' do @@ -1313,6 +1285,46 @@ module Gitlab }) end end + + context 'when image and service have docker options' do + let(:config) do + <<~YAML + test: + script: exit 0 + image: + name: ruby:2.7 + docker: + platform: linux/amd64 + services: + - name: postgres:11.9 + docker: + platform: linux/amd64 + YAML + end + + it { is_expected.to be_valid } + + it "returns with image" do + expect(processor.builds).to contain_exactly({ + stage: "test", + stage_idx: 2, + name: "test", + only: { refs: %w[branches tags] }, + options: { + script: ["exit 0"], + image: { name: "ruby:2.7", + executor_opts: { docker: { platform: 'linux/amd64' } } }, + services: [{ name: "postgres:11.9", + executor_opts: { docker: { platform: 'linux/amd64' } } }] + }, + allow_failure: false, + when: "on_success", + job_variables: [], + root_variables_inheritance: true, + scheduling_type: :stage + }) + end + end end describe 'Variables' do |