diff options
Diffstat (limited to 'spec/lib/gitlab/ci/config')
18 files changed, 874 insertions, 72 deletions
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 |