diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-05-17 16:05:49 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-05-17 16:05:49 +0000 |
commit | 43a25d93ebdabea52f99b05e15b06250cd8f07d7 (patch) | |
tree | dceebdc68925362117480a5d672bcff122fb625b /spec/lib/gitlab/ci | |
parent | 20c84b99005abd1c82101dfeff264ac50d2df211 (diff) | |
download | gitlab-ce-16.0.0-rc42.tar.gz |
Add latest changes from gitlab-org/gitlab@16-0-stable-eev16.0.0-rc4216-0-stable
Diffstat (limited to 'spec/lib/gitlab/ci')
89 files changed, 3321 insertions, 656 deletions
diff --git a/spec/lib/gitlab/ci/ansi2json/state_spec.rb b/spec/lib/gitlab/ci/ansi2json/state_spec.rb new file mode 100644 index 00000000000..8dd4092f3d8 --- /dev/null +++ b/spec/lib/gitlab/ci/ansi2json/state_spec.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Ansi2json::State, feature_category: :continuous_integration do + def build_state + described_class.new('', 1000).tap do |state| + state.offset = 1 + state.new_line!(style: { fg: 'some-fg', bg: 'some-bg', mask: 1234 }) + state.set_last_line_offset + state.open_section('hello', 111, {}) + end + end + + let(:state) { build_state } + + describe '#initialize' do + it 'restores valid prior state', :aggregate_failures do + new_state = described_class.new(state.encode, 1000) + + expect(new_state.offset).to eq(1) + expect(new_state.inherited_style).to eq({ + bg: 'some-bg', + fg: 'some-fg', + mask: 1234 + }) + expect(new_state.open_sections).to eq({ 'hello' => 111 }) + end + + it 'ignores unsigned prior state', :aggregate_failures do + unsigned, _ = build_state.encode.split('--') + + expect(::Gitlab::AppLogger).to( + receive(:warn).with( + message: a_string_matching(/signature missing or invalid/), + invalid_state: unsigned + ) + ) + + new_state = described_class.new(unsigned, 0) + + expect(new_state.offset).to eq(0) + expect(new_state.inherited_style).to eq({}) + expect(new_state.open_sections).to eq({}) + end + + it 'ignores bad input', :aggregate_failures do + expect(::Gitlab::AppLogger).to( + receive(:warn).with( + message: a_string_matching(/signature missing or invalid/), + invalid_state: 'abcd' + ) + ) + + new_state = described_class.new('abcd', 0) + + expect(new_state.offset).to eq(0) + expect(new_state.inherited_style).to eq({}) + expect(new_state.open_sections).to eq({}) + end + end + + describe '#encode' do + it 'deterministically signs the state' do + expect(state.encode).to eq state.encode + end + end +end diff --git a/spec/lib/gitlab/ci/ansi2json_spec.rb b/spec/lib/gitlab/ci/ansi2json_spec.rb index 0f8f3759834..98fca40e8ea 100644 --- a/spec/lib/gitlab/ci/ansi2json_spec.rb +++ b/spec/lib/gitlab/ci/ansi2json_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::Ansi2json do +RSpec.describe Gitlab::Ci::Ansi2json, feature_category: :continuous_integration do subject { described_class } describe 'lines' do diff --git a/spec/lib/gitlab/ci/badge/release/template_spec.rb b/spec/lib/gitlab/ci/badge/release/template_spec.rb index 2b66c296a94..6be0dcaae99 100644 --- a/spec/lib/gitlab/ci/badge/release/template_spec.rb +++ b/spec/lib/gitlab/ci/badge/release/template_spec.rb @@ -59,9 +59,30 @@ RSpec.describe Gitlab::Ci::Badge::Release::Template do end describe '#value_width' do - it 'has a fixed value width' do + it 'returns the default value width' do expect(template.value_width).to eq 54 end + + it 'returns custom value width' do + value_width = 100 + badge = Gitlab::Ci::Badge::Release::LatestRelease.new(project, user, opts: { value_width: value_width }) + + expect(described_class.new(badge).value_width).to eq value_width + end + + it 'returns VALUE_WIDTH_DEFAULT if the custom value_width supplied is greater than permissible limit' do + value_width = 250 + badge = Gitlab::Ci::Badge::Release::LatestRelease.new(project, user, opts: { value_width: value_width }) + + expect(described_class.new(badge).value_width).to eq 54 + end + + it 'returns VALUE_WIDTH_DEFAULT if value_width is not a number' do + value_width = "string" + badge = Gitlab::Ci::Badge::Release::LatestRelease.new(project, user, opts: { value_width: value_width }) + + expect(described_class.new(badge).value_width).to eq 54 + end end describe '#key_color' do diff --git a/spec/lib/gitlab/ci/build/auto_retry_spec.rb b/spec/lib/gitlab/ci/build/auto_retry_spec.rb index 314714c543b..0b275e7d564 100644 --- a/spec/lib/gitlab/ci/build/auto_retry_spec.rb +++ b/spec/lib/gitlab/ci/build/auto_retry_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::Build::AutoRetry, feature_category: :pipeline_authoring do +RSpec.describe Gitlab::Ci::Build::AutoRetry, feature_category: :pipeline_composition do let(:auto_retry) { described_class.new(build) } describe '#allowed?' do diff --git a/spec/lib/gitlab/ci/build/cache_spec.rb b/spec/lib/gitlab/ci/build/cache_spec.rb index a8fa14b4b4c..68d6a7978d7 100644 --- a/spec/lib/gitlab/ci/build/cache_spec.rb +++ b/spec/lib/gitlab/ci/build/cache_spec.rb @@ -3,16 +3,21 @@ require 'spec_helper' RSpec.describe Gitlab::Ci::Build::Cache do + let(:cache_config) { [] } + let(:pipeline) { double(::Ci::Pipeline) } + let(:cache_seed_a) { double(Gitlab::Ci::Pipeline::Seed::Build::Cache) } + let(:cache_seed_b) { double(Gitlab::Ci::Pipeline::Seed::Build::Cache) } + + subject(:cache) { described_class.new(cache_config, pipeline) } + describe '.initialize' do context 'when the cache is an array' do + let(:cache_config) { [{ key: 'key-a' }, { key: 'key-b' }] } + it 'instantiates an array of cache seeds' do - cache_config = [{ key: 'key-a' }, { key: 'key-b' }] - pipeline = double(::Ci::Pipeline) - cache_seed_a = double(Gitlab::Ci::Pipeline::Seed::Build::Cache) - cache_seed_b = double(Gitlab::Ci::Pipeline::Seed::Build::Cache) allow(Gitlab::Ci::Pipeline::Seed::Build::Cache).to receive(:new).and_return(cache_seed_a, cache_seed_b) - cache = described_class.new(cache_config, pipeline) + cache expect(Gitlab::Ci::Pipeline::Seed::Build::Cache).to have_received(:new).with(pipeline, { key: 'key-a' }, 0) expect(Gitlab::Ci::Pipeline::Seed::Build::Cache).to have_received(:new).with(pipeline, { key: 'key-b' }, 1) @@ -21,16 +26,31 @@ RSpec.describe Gitlab::Ci::Build::Cache do end context 'when the cache is a hash' do + let(:cache_config) { { key: 'key-a' } } + it 'instantiates a cache seed' do - cache_config = { key: 'key-a' } - pipeline = double(::Ci::Pipeline) - cache_seed = double(Gitlab::Ci::Pipeline::Seed::Build::Cache) - allow(Gitlab::Ci::Pipeline::Seed::Build::Cache).to receive(:new).and_return(cache_seed) + allow(Gitlab::Ci::Pipeline::Seed::Build::Cache).to receive(:new).and_return(cache_seed_a) - cache = described_class.new(cache_config, pipeline) + cache expect(Gitlab::Ci::Pipeline::Seed::Build::Cache).to have_received(:new).with(pipeline, cache_config, 0) - expect(cache.instance_variable_get(:@cache)).to eq([cache_seed]) + expect(cache.instance_variable_get(:@cache)).to eq([cache_seed_a]) + end + end + + context 'when the cache is an array with files inside hashes' do + let(:cache_config) { [{ key: { files: ['file1.json'] } }, { key: { files: ['file1.json', 'file2.json'] } }] } + + it 'instantiates a cache seed' do + allow(Gitlab::Ci::Pipeline::Seed::Build::Cache).to receive(:new).and_return(cache_seed_a, cache_seed_b) + + cache + + expect(Gitlab::Ci::Pipeline::Seed::Build::Cache).to have_received(:new) + .with(pipeline, cache_config.first, '0_file1') + expect(Gitlab::Ci::Pipeline::Seed::Build::Cache).to have_received(:new) + .with(pipeline, cache_config.second, '1_file1_file2') + expect(cache.instance_variable_get(:@cache)).to match_array([cache_seed_a, cache_seed_b]) end end end @@ -38,10 +58,6 @@ RSpec.describe Gitlab::Ci::Build::Cache do describe '#cache_attributes' do context 'when there are no caches' do it 'returns an empty hash' do - cache_config = [] - pipeline = double(::Ci::Pipeline) - cache = described_class.new(cache_config, pipeline) - attributes = cache.cache_attributes expect(attributes).to eq({}) @@ -51,7 +67,6 @@ RSpec.describe Gitlab::Ci::Build::Cache do context 'when there are caches' do it 'returns the structured attributes for the caches' do cache_config = [{ key: 'key-a' }, { key: 'key-b' }] - pipeline = double(::Ci::Pipeline) cache = described_class.new(cache_config, pipeline) attributes = cache.cache_attributes diff --git a/spec/lib/gitlab/ci/build/context/build_spec.rb b/spec/lib/gitlab/ci/build/context/build_spec.rb index 74739a67be0..d4a2af0015f 100644 --- a/spec/lib/gitlab/ci/build/context/build_spec.rb +++ b/spec/lib/gitlab/ci/build/context/build_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::Build::Context::Build, feature_category: :pipeline_authoring do +RSpec.describe Gitlab::Ci::Build::Context::Build, feature_category: :pipeline_composition do let(:pipeline) { create(:ci_pipeline) } let(:seed_attributes) { { 'name' => 'some-job' } } @@ -13,14 +13,29 @@ RSpec.describe Gitlab::Ci::Build::Context::Build, feature_category: :pipeline_au it { is_expected.to include('CI_PIPELINE_IID' => pipeline.iid.to_s) } it { is_expected.to include('CI_PROJECT_PATH' => pipeline.project.full_path) } it { is_expected.to include('CI_JOB_NAME' => 'some-job') } - it { is_expected.to include('CI_BUILD_REF_NAME' => 'master') } + + context 'when FF `ci_remove_legacy_predefined_variables` is disabled' do + before do + stub_feature_flags(ci_remove_legacy_predefined_variables: false) + end + + it { is_expected.to include('CI_BUILD_REF_NAME' => 'master') } + end context 'without passed build-specific attributes' do let(:context) { described_class.new(pipeline) } - it { is_expected.to include('CI_JOB_NAME' => nil) } - it { is_expected.to include('CI_BUILD_REF_NAME' => 'master') } - it { is_expected.to include('CI_PROJECT_PATH' => pipeline.project.full_path) } + it { is_expected.to include('CI_JOB_NAME' => nil) } + it { is_expected.to include('CI_COMMIT_REF_NAME' => 'master') } + it { is_expected.to include('CI_PROJECT_PATH' => pipeline.project.full_path) } + + context 'when FF `ci_remove_legacy_predefined_variables` is disabled' do + before do + stub_feature_flags(ci_remove_legacy_predefined_variables: false) + end + + it { is_expected.to include('CI_BUILD_REF_NAME' => 'master') } + end end context 'when environment:name is provided' do diff --git a/spec/lib/gitlab/ci/build/context/global_spec.rb b/spec/lib/gitlab/ci/build/context/global_spec.rb index d4141eb8389..328b5eb62fa 100644 --- a/spec/lib/gitlab/ci/build/context/global_spec.rb +++ b/spec/lib/gitlab/ci/build/context/global_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::Build::Context::Global do +RSpec.describe Gitlab::Ci::Build::Context::Global, feature_category: :pipeline_composition do let(:pipeline) { create(:ci_pipeline) } let(:yaml_variables) { {} } @@ -14,7 +14,14 @@ RSpec.describe Gitlab::Ci::Build::Context::Global do it { is_expected.to include('CI_PROJECT_PATH' => pipeline.project.full_path) } it { is_expected.not_to have_key('CI_JOB_NAME') } - it { is_expected.not_to have_key('CI_BUILD_REF_NAME') } + + context 'when FF `ci_remove_legacy_predefined_variables` is disabled' do + before do + stub_feature_flags(ci_remove_legacy_predefined_variables: false) + end + + it { is_expected.not_to have_key('CI_BUILD_REF_NAME') } + end context 'with passed yaml variables' do let(:yaml_variables) { [{ key: 'SUPPORTED', value: 'parsed', public: true }] } diff --git a/spec/lib/gitlab/ci/build/hook_spec.rb b/spec/lib/gitlab/ci/build/hook_spec.rb index 6ed40a44c97..6c9175b4260 100644 --- a/spec/lib/gitlab/ci/build/hook_spec.rb +++ b/spec/lib/gitlab/ci/build/hook_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::Build::Hook, feature_category: :pipeline_authoring do +RSpec.describe Gitlab::Ci::Build::Hook, feature_category: :pipeline_composition do let_it_be(:build1) do FactoryBot.build(:ci_build, options: { hooks: { pre_get_sources_script: ["echo 'hello pre_get_sources_script'"] } }) diff --git a/spec/lib/gitlab/ci/build/rules_spec.rb b/spec/lib/gitlab/ci/build/rules_spec.rb index e82dcd0254d..1ece0f6b7b9 100644 --- a/spec/lib/gitlab/ci/build/rules_spec.rb +++ b/spec/lib/gitlab/ci/build/rules_spec.rb @@ -181,6 +181,108 @@ RSpec.describe Gitlab::Ci::Build::Rules do end end + context 'with needs' do + context 'when single needs is specified' do + let(:rule_list) do + [{ if: '$VAR == null', needs: [{ name: 'test', artifacts: true, optional: false }] }] + end + + it { + is_expected.to eq(described_class::Result.new('on_success', nil, nil, nil, + [{ name: 'test', artifacts: true, optional: false }], nil)) + } + end + + context 'when multiple needs are specified' do + let(:rule_list) do + [{ if: '$VAR == null', + needs: [{ name: 'test', artifacts: true, optional: false }, + { name: 'rspec', artifacts: true, optional: false }] }] + end + + it { + is_expected.to eq(described_class::Result.new('on_success', nil, nil, nil, + [{ name: 'test', artifacts: true, optional: false }, + { name: 'rspec', artifacts: true, optional: false }], nil)) + } + end + + context 'when there are no needs specified' do + let(:rule_list) { [{ if: '$VAR == null' }] } + + it { is_expected.to eq(described_class::Result.new('on_success', nil, nil, nil, nil, nil)) } + end + + context 'when need is specified with additional attibutes' do + let(:rule_list) do + [{ if: '$VAR == null', needs: [{ + artifacts: true, + name: 'test', + optional: false, + when: 'never' + }] }] + end + + it { + is_expected.to eq( + described_class::Result.new('on_success', nil, nil, nil, + [{ artifacts: true, name: 'test', optional: false, when: 'never' }], nil)) + } + end + + context 'when feature flag is disabled' do + before do + stub_feature_flags(introduce_rules_with_needs: false) + end + + context 'with needs' do + context 'when single needs is specified' do + let(:rule_list) do + [{ if: '$VAR == null', needs: [{ name: 'test', artifacts: true, optional: false }] }] + end + + it { + is_expected.to eq(described_class::Result.new('on_success', nil, nil, nil, nil, nil)) + } + end + + context 'when multiple needs are specified' do + let(:rule_list) do + [{ if: '$VAR == null', + needs: [{ name: 'test', artifacts: true, optional: false }, + { name: 'rspec', artifacts: true, optional: false }] }] + end + + it { + is_expected.to eq(described_class::Result.new('on_success', nil, nil, nil, nil, nil)) + } + end + + context 'when there are no needs specified' do + let(:rule_list) { [{ if: '$VAR == null' }] } + + it { is_expected.to eq(described_class::Result.new('on_success', nil, nil, nil, nil, nil)) } + end + + context 'when need is specified with additional attibutes' do + let(:rule_list) do + [{ if: '$VAR == null', needs: [{ + artifacts: true, + name: 'test', + optional: false, + when: 'never' + }] }] + end + + it { + is_expected.to eq( + described_class::Result.new('on_success', nil, nil, nil, nil, nil)) + } + end + end + end + end + context 'with variables' do context 'with matching rule' do let(:rule_list) { [{ if: '$VAR == null', variables: { MY_VAR: 'my var' } }] } @@ -208,9 +310,10 @@ RSpec.describe Gitlab::Ci::Build::Rules do let(:start_in) { nil } let(:allow_failure) { nil } let(:variables) { nil } + let(:needs) { nil } subject(:result) do - Gitlab::Ci::Build::Rules::Result.new(when_value, start_in, allow_failure, variables) + Gitlab::Ci::Build::Rules::Result.new(when_value, start_in, allow_failure, variables, needs) end describe '#build_attributes' do @@ -221,6 +324,45 @@ RSpec.describe Gitlab::Ci::Build::Rules do it 'compacts nil values' do is_expected.to eq(options: {}, when: 'on_success') end + + context 'scheduling_type' do + context 'when rules have needs' do + context 'single need' do + let(:needs) do + { job: [{ name: 'test' }] } + end + + it 'saves needs' do + expect(subject[:needs_attributes]).to eq([{ name: "test" }]) + end + + it 'adds schedule type to the build_attributes' do + expect(subject[:scheduling_type]).to eq(:dag) + end + end + + context 'multiple needs' do + let(:needs) do + { job: [{ name: 'test' }, { name: 'test_2', artifacts: true, optional: false }] } + end + + it 'saves needs' do + expect(subject[:needs_attributes]).to match_array([{ name: "test" }, + { name: 'test_2', artifacts: true, optional: false }]) + end + + it 'adds schedule type to the build_attributes' do + expect(subject[:scheduling_type]).to eq(:dag) + end + end + end + + context 'when rules do not have needs' do + it 'does not add schedule type to the build_attributes' do + expect(subject.key?(:scheduling_type)).to be_falsy + end + end + end end describe '#pass?' do diff --git a/spec/lib/gitlab/ci/components/instance_path_spec.rb b/spec/lib/gitlab/ci/components/instance_path_spec.rb index d9beae0555c..b80422d03e5 100644 --- a/spec/lib/gitlab/ci/components/instance_path_spec.rb +++ b/spec/lib/gitlab/ci/components/instance_path_spec.rb @@ -2,11 +2,11 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::Components::InstancePath, feature_category: :pipeline_authoring do +RSpec.describe Gitlab::Ci::Components::InstancePath, feature_category: :pipeline_composition do let_it_be(:user) { create(:user) } let(:path) { described_class.new(address: address, content_filename: 'template.yml') } - let(:settings) { Settingslogic.new({ 'component_fqdn' => current_host }) } + let(:settings) { GitlabSettings::Options.build({ 'component_fqdn' => current_host }) } let(:current_host) { 'acme.com/' } before do @@ -98,6 +98,37 @@ RSpec.describe Gitlab::Ci::Components::InstancePath, feature_category: :pipeline end end + context 'when version is `~latest`' do + let(:version) { '~latest' } + + context 'when project is a catalog resource' do + before do + create(:catalog_resource, project: existing_project) + end + + context 'when project has releases' do + let_it_be(:releases) do + [ + create(:release, project: existing_project, sha: 'sha-1', released_at: Time.zone.now - 1.day), + create(:release, project: existing_project, sha: 'sha-2', released_at: Time.zone.now) + ] + end + + it 'returns the sha of the latest release' do + expect(path.sha).to eq(releases.last.sha) + end + end + + context 'when project does not have releases' do + it { expect(path.sha).to be_nil } + end + end + + context 'when project is not a catalog resource' do + it { expect(path.sha).to be_nil } + end + end + context 'when project does not exist' do let(:project_path) { 'non-existent/project' } diff --git a/spec/lib/gitlab/ci/config/entry/cache_spec.rb b/spec/lib/gitlab/ci/config/entry/cache_spec.rb index 67252eed938..82db116fa0d 100644 --- a/spec/lib/gitlab/ci/config/entry/cache_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/cache_spec.rb @@ -17,6 +17,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Cache do let(:key) { 'some key' } let(:when_config) { nil } let(:unprotect) { false } + let(:fallback_keys) { [] } let(:config) do { @@ -27,13 +28,22 @@ RSpec.describe Gitlab::Ci::Config::Entry::Cache do }.tap do |config| config[:policy] = policy if policy config[:when] = when_config if when_config + config[:fallback_keys] = fallback_keys if fallback_keys end end describe '#value' do shared_examples 'hash key value' do it 'returns hash value' do - expect(entry.value).to eq(key: key, untracked: true, paths: ['some/path/'], policy: 'pull-push', when: 'on_success', unprotect: false) + expect(entry.value).to eq( + key: key, + untracked: true, + paths: ['some/path/'], + policy: 'pull-push', + when: 'on_success', + unprotect: false, + fallback_keys: [] + ) end end @@ -104,6 +114,20 @@ RSpec.describe Gitlab::Ci::Config::Entry::Cache do expect(entry.value).to include(when: 'on_success') end end + + context 'with `fallback_keys`' do + let(:fallback_keys) { %w[key-1 key-2] } + + it 'matches the list of fallback keys' do + expect(entry.value).to match(a_hash_including(fallback_keys: %w[key-1 key-2])) + end + end + + context 'without `fallback_keys`' do + it 'assigns an empty list' do + expect(entry.value).to match(a_hash_including(fallback_keys: [])) + end + end end describe '#valid?' do diff --git a/spec/lib/gitlab/ci/config/entry/job_spec.rb b/spec/lib/gitlab/ci/config/entry/job_spec.rb index c1b9bd58d98..4be7c11fab0 100644 --- a/spec/lib/gitlab/ci/config/entry/job_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/job_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::Config::Entry::Job, feature_category: :pipeline_authoring do +RSpec.describe Gitlab::Ci::Config::Entry::Job, feature_category: :pipeline_composition do let(:entry) { described_class.new(config, name: :rspec) } it_behaves_like 'with inheritable CI config' do @@ -261,13 +261,13 @@ RSpec.describe Gitlab::Ci::Config::Entry::Job, feature_category: :pipeline_autho end end - context 'when it is lower than two' do - let(:config) { { script: 'echo', parallel: 1 } } + context 'when it is lower than one' do + let(:config) { { script: 'echo', parallel: 0 } } it 'returns error about value too low' do expect(entry).not_to be_valid expect(entry.errors) - .to include 'parallel config must be greater than or equal to 2' + .to include 'parallel config must be greater than or equal to 1' end end @@ -595,6 +595,39 @@ RSpec.describe Gitlab::Ci::Config::Entry::Job, feature_category: :pipeline_autho end end end + + context 'when job is not a pages job' do + let(:name) { :rspec } + + context 'if the config contains a publish entry' do + let(:entry) { described_class.new({ script: 'echo', publish: 'foo' }, name: name) } + + it 'is invalid' do + expect(entry).not_to be_valid + expect(entry.errors).to include /job publish can only be used within a `pages` job/ + end + end + end + + context 'when job is a pages job' do + let(:name) { :pages } + + context 'when it does not have a publish entry' do + let(:entry) { described_class.new({ script: 'echo' }, name: name) } + + it 'is valid' do + expect(entry).to be_valid + end + end + + context 'when it has a publish entry' do + let(:entry) { described_class.new({ script: 'echo', publish: 'foo' }, name: name) } + + it 'is valid' do + expect(entry).to be_valid + end + end + end end describe '#relevant?' do @@ -631,7 +664,13 @@ RSpec.describe Gitlab::Ci::Config::Entry::Job, feature_category: :pipeline_autho it 'overrides default config' do expect(entry[:image].value).to eq(name: 'some_image') - expect(entry[:cache].value).to eq([key: 'test', policy: 'pull-push', when: 'on_success', unprotect: false]) + expect(entry[:cache].value).to match_array([ + key: 'test', + policy: 'pull-push', + when: 'on_success', + unprotect: false, + fallback_keys: [] + ]) end end @@ -646,7 +685,13 @@ RSpec.describe Gitlab::Ci::Config::Entry::Job, feature_category: :pipeline_autho it 'uses config from default entry' do expect(entry[:image].value).to eq 'specified' - expect(entry[:cache].value).to eq([key: 'test', policy: 'pull-push', when: 'on_success', unprotect: false]) + expect(entry[:cache].value).to match_array([ + key: 'test', + policy: 'pull-push', + when: 'on_success', + unprotect: false, + fallback_keys: [] + ]) end end @@ -728,27 +773,6 @@ RSpec.describe Gitlab::Ci::Config::Entry::Job, feature_category: :pipeline_autho scheduling_type: :stage, id_tokens: { TEST_ID_TOKEN: { aud: 'https://gitlab.com' } }) end - - context 'when the FF ci_hooks_pre_get_sources_script is disabled' do - before do - stub_feature_flags(ci_hooks_pre_get_sources_script: false) - end - - it 'returns correct value' do - expect(entry.value) - .to eq(name: :rspec, - before_script: %w[ls pwd], - script: %w[rspec], - stage: 'test', - ignore: false, - after_script: %w[cleanup], - only: { refs: %w[branches tags] }, - job_variables: {}, - root_variables_inheritance: true, - scheduling_type: :stage, - id_tokens: { TEST_ID_TOKEN: { aud: 'https://gitlab.com' } }) - end - end end end diff --git a/spec/lib/gitlab/ci/config/entry/policy_spec.rb b/spec/lib/gitlab/ci/config/entry/policy_spec.rb index 378c0947e8a..7093a0a6edf 100644 --- a/spec/lib/gitlab/ci/config/entry/policy_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/policy_spec.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true -require 'fast_spec_helper' +require 'spec_helper' -RSpec.describe Gitlab::Ci::Config::Entry::Policy do +RSpec.describe Gitlab::Ci::Config::Entry::Policy, feature_category: :continuous_integration do let(:entry) { described_class.new(config) } context 'when using simplified policy' do diff --git a/spec/lib/gitlab/ci/config/entry/processable_spec.rb b/spec/lib/gitlab/ci/config/entry/processable_spec.rb index b28562ba2ea..4f13940d7e2 100644 --- a/spec/lib/gitlab/ci/config/entry/processable_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/processable_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::Config::Entry::Processable, feature_category: :pipeline_authoring do +RSpec.describe Gitlab::Ci::Config::Entry::Processable, feature_category: :pipeline_composition do let(:node_class) do Class.new(::Gitlab::Config::Entry::Node) do include Gitlab::Ci::Config::Entry::Processable diff --git a/spec/lib/gitlab/ci/config/entry/product/parallel_spec.rb b/spec/lib/gitlab/ci/config/entry/product/parallel_spec.rb index ec21519a8f6..1025c41477d 100644 --- a/spec/lib/gitlab/ci/config/entry/product/parallel_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/product/parallel_spec.rb @@ -27,10 +27,10 @@ RSpec.describe ::Gitlab::Ci::Config::Entry::Product::Parallel do it_behaves_like 'invalid config', /should be an integer or a hash/ end - context 'when it is lower than two' do - let(:config) { 1 } + context 'when it is lower than one' do + let(:config) { 0 } - it_behaves_like 'invalid config', /must be greater than or equal to 2/ + it_behaves_like 'invalid config', /must be greater than or equal to 1/ end context 'when it is bigger than 200' do diff --git a/spec/lib/gitlab/ci/config/entry/publish_spec.rb b/spec/lib/gitlab/ci/config/entry/publish_spec.rb new file mode 100644 index 00000000000..53ad868a05e --- /dev/null +++ b/spec/lib/gitlab/ci/config/entry/publish_spec.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Config::Entry::Publish, feature_category: :pages do + let(:publish) { described_class.new(config) } + + describe 'validations' do + context 'when publish config value is correct' do + let(:config) { 'dist/static' } + + describe '#config' do + it 'returns the publish directory' do + expect(publish.config).to eq config + end + end + + describe '#valid?' do + it 'is valid' do + expect(publish).to be_valid + end + end + end + + context 'when the value has a wrong type' do + let(:config) { { test: true } } + + it 'reports an error' do + expect(publish.errors) + .to include 'publish config should be a string' + end + end + end + + describe '.default' do + it 'returns the default value' do + expect(described_class.default).to eq 'public' + end + end +end diff --git a/spec/lib/gitlab/ci/config/entry/pull_policy_spec.rb b/spec/lib/gitlab/ci/config/entry/pull_policy_spec.rb index c35355b10c6..40507a66c2d 100644 --- a/spec/lib/gitlab/ci/config/entry/pull_policy_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/pull_policy_spec.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true -require 'fast_spec_helper' +require 'spec_helper' -RSpec.describe Gitlab::Ci::Config::Entry::PullPolicy do +RSpec.describe Gitlab::Ci::Config::Entry::PullPolicy, feature_category: :continuous_integration do let(:entry) { described_class.new(config) } describe '#value' do diff --git a/spec/lib/gitlab/ci/config/entry/reports/coverage_report_spec.rb b/spec/lib/gitlab/ci/config/entry/reports/coverage_report_spec.rb index ccd6f6ab427..6f37dd72083 100644 --- a/spec/lib/gitlab/ci/config/entry/reports/coverage_report_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/reports/coverage_report_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::Config::Entry::Reports::CoverageReport, feature_category: :pipeline_authoring do +RSpec.describe Gitlab::Ci::Config::Entry::Reports::CoverageReport, feature_category: :pipeline_composition do let(:entry) { described_class.new(config) } describe 'validations' do diff --git a/spec/lib/gitlab/ci/config/entry/reports_spec.rb b/spec/lib/gitlab/ci/config/entry/reports_spec.rb index 715cb18fb92..73bf2d422b7 100644 --- a/spec/lib/gitlab/ci/config/entry/reports_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/reports_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::Config::Entry::Reports, feature_category: :pipeline_authoring do +RSpec.describe Gitlab::Ci::Config::Entry::Reports, feature_category: :pipeline_composition do let(:entry) { described_class.new(config) } describe 'validates ALLOWED_KEYS' do diff --git a/spec/lib/gitlab/ci/config/entry/root_spec.rb b/spec/lib/gitlab/ci/config/entry/root_spec.rb index 9722609aef6..5fac5298e8e 100644 --- a/spec/lib/gitlab/ci/config/entry/root_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/root_spec.rb @@ -128,7 +128,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do services: [{ name: 'postgres:9.1' }, { name: 'mysql:5.5' }], stage: 'test', cache: [{ key: 'k', untracked: true, paths: ['public/'], policy: 'pull-push', when: 'on_success', - unprotect: false }], + unprotect: false, fallback_keys: [] }], job_variables: {}, root_variables_inheritance: true, ignore: false, @@ -144,7 +144,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do services: [{ name: 'postgres:9.1' }, { name: 'mysql:5.5' }], stage: 'test', cache: [{ key: 'k', untracked: true, paths: ['public/'], policy: 'pull-push', when: 'on_success', - unprotect: false }], + unprotect: false, fallback_keys: [] }], job_variables: {}, root_variables_inheritance: true, ignore: false, @@ -161,7 +161,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do image: { name: "image:1.0" }, services: [{ name: "postgres:9.1" }, { name: "mysql:5.5" }], cache: [{ key: "k", untracked: true, paths: ["public/"], policy: "pull-push", when: 'on_success', - unprotect: false }], + unprotect: false, fallback_keys: [] }], only: { refs: %w(branches tags) }, job_variables: { 'VAR' => { value: 'job' } }, root_variables_inheritance: true, @@ -209,7 +209,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do image: { name: 'image:1.0' }, services: [{ name: 'postgres:9.1' }, { name: 'mysql:5.5' }], stage: 'test', - cache: [{ key: 'k', untracked: true, paths: ['public/'], policy: 'pull-push', when: 'on_success', unprotect: false }], + cache: [{ key: 'k', untracked: true, paths: ['public/'], policy: 'pull-push', when: 'on_success', unprotect: false, fallback_keys: [] }], job_variables: {}, root_variables_inheritance: true, ignore: false, @@ -222,7 +222,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do image: { name: 'image:1.0' }, services: [{ name: 'postgres:9.1' }, { name: 'mysql:5.5' }], stage: 'test', - cache: [{ key: 'k', untracked: true, paths: ['public/'], policy: 'pull-push', when: 'on_success', unprotect: false }], + cache: [{ key: 'k', untracked: true, paths: ['public/'], policy: 'pull-push', when: 'on_success', unprotect: false, fallback_keys: [] }], job_variables: { 'VAR' => { value: 'job' } }, root_variables_inheritance: true, ignore: false, @@ -277,7 +277,13 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do describe '#cache_value' do it 'returns correct cache definition' do - expect(root.cache_value).to eq([key: 'a', policy: 'pull-push', when: 'on_success', unprotect: false]) + expect(root.cache_value).to match_array([ + key: 'a', + policy: 'pull-push', + when: 'on_success', + unprotect: false, + fallback_keys: [] + ]) end end end diff --git a/spec/lib/gitlab/ci/config/entry/trigger_spec.rb b/spec/lib/gitlab/ci/config/entry/trigger_spec.rb index f47923af45a..fdd598c2ab2 100644 --- a/spec/lib/gitlab/ci/config/entry/trigger_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/trigger_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::Config::Entry::Trigger, feature_category: :pipeline_authoring do +RSpec.describe Gitlab::Ci::Config::Entry::Trigger, feature_category: :pipeline_composition do subject { described_class.new(config) } context 'when trigger config is a non-empty string' do diff --git a/spec/lib/gitlab/ci/config/external/context_spec.rb b/spec/lib/gitlab/ci/config/external/context_spec.rb index 1fd3cf3c99f..d917924f257 100644 --- a/spec/lib/gitlab/ci/config/external/context_spec.rb +++ b/spec/lib/gitlab/ci/config/external/context_spec.rb @@ -2,12 +2,21 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::Config::External::Context, feature_category: :pipeline_authoring do +RSpec.describe Gitlab::Ci::Config::External::Context, feature_category: :pipeline_composition do let(:project) { build(:project) } let(:user) { double('User') } let(:sha) { '12345' } let(:variables) { Gitlab::Ci::Variables::Collection.new([{ 'key' => 'a', 'value' => 'b' }]) } - let(:attributes) { { project: project, user: user, sha: sha, variables: variables } } + let(:pipeline_config) { instance_double(Gitlab::Ci::ProjectConfig) } + let(:attributes) do + { + project: project, + user: user, + sha: sha, + variables: variables, + pipeline_config: pipeline_config + } + end subject(:subject) { described_class.new(**attributes) } @@ -15,11 +24,11 @@ RSpec.describe Gitlab::Ci::Config::External::Context, feature_category: :pipelin context 'with values' do it { is_expected.to have_attributes(**attributes) } it { expect(subject.expandset).to eq([]) } - it { expect(subject.max_includes).to eq(Gitlab::Ci::Config::External::Context::NEW_MAX_INCLUDES) } it { expect(subject.execution_deadline).to eq(0) } it { expect(subject.variables).to be_instance_of(Gitlab::Ci::Variables::Collection) } it { expect(subject.variables_hash).to be_instance_of(ActiveSupport::HashWithIndifferentAccess) } it { expect(subject.variables_hash).to include('a' => 'b') } + it { expect(subject.pipeline_config).to eq(pipeline_config) } end context 'without values' do @@ -27,36 +36,25 @@ RSpec.describe Gitlab::Ci::Config::External::Context, feature_category: :pipelin it { is_expected.to have_attributes(**attributes) } it { expect(subject.expandset).to eq([]) } - it { expect(subject.max_includes).to eq(Gitlab::Ci::Config::External::Context::NEW_MAX_INCLUDES) } it { expect(subject.execution_deadline).to eq(0) } it { expect(subject.variables).to be_instance_of(Gitlab::Ci::Variables::Collection) } it { expect(subject.variables_hash).to be_instance_of(ActiveSupport::HashWithIndifferentAccess) } + it { expect(subject.pipeline_config).to be_nil } end - context 'when FF ci_includes_count_duplicates is disabled' do - before do - stub_feature_flags(ci_includes_count_duplicates: false) - end - - context 'with values' do - it { is_expected.to have_attributes(**attributes) } - it { expect(subject.expandset).to eq(Set.new) } - it { expect(subject.max_includes).to eq(Gitlab::Ci::Config::External::Context::MAX_INCLUDES) } - it { expect(subject.execution_deadline).to eq(0) } - it { expect(subject.variables).to be_instance_of(Gitlab::Ci::Variables::Collection) } - it { expect(subject.variables_hash).to be_instance_of(ActiveSupport::HashWithIndifferentAccess) } - it { expect(subject.variables_hash).to include('a' => 'b') } + describe 'max_includes' do + it 'returns the default value of application setting `ci_max_includes`' do + expect(subject.max_includes).to eq(150) end - context 'without values' do - let(:attributes) { { project: nil, user: nil, sha: nil } } + context 'when application setting `ci_max_includes` is changed' do + before do + stub_application_setting(ci_max_includes: 200) + end - it { is_expected.to have_attributes(**attributes) } - it { expect(subject.expandset).to eq(Set.new) } - it { expect(subject.max_includes).to eq(Gitlab::Ci::Config::External::Context::MAX_INCLUDES) } - it { expect(subject.execution_deadline).to eq(0) } - it { expect(subject.variables).to be_instance_of(Gitlab::Ci::Variables::Collection) } - it { expect(subject.variables_hash).to be_instance_of(ActiveSupport::HashWithIndifferentAccess) } + it 'returns the new value of application setting `ci_max_includes`' do + expect(subject.max_includes).to eq(200) + end end end end @@ -170,4 +168,26 @@ RSpec.describe Gitlab::Ci::Config::External::Context, feature_category: :pipelin describe '#sentry_payload' do it { expect(subject.sentry_payload).to match(a_hash_including(:project, :user)) } end + + describe '#internal_include?' do + context 'when pipeline_config is provided' do + where(:value) { [true, false] } + + with_them do + it 'returns the value of .internal_include_prepended?' do + allow(pipeline_config).to receive(:internal_include_prepended?).and_return(value) + + expect(subject.internal_include?).to eq(value) + end + end + end + + context 'when pipeline_config is not provided' do + let(:pipeline_config) { nil } + + it 'returns false' do + expect(subject.internal_include?).to eq(false) + end + end + end end diff --git a/spec/lib/gitlab/ci/config/external/file/artifact_spec.rb b/spec/lib/gitlab/ci/config/external/file/artifact_spec.rb index 45a15fb5f36..087dacd5ef0 100644 --- a/spec/lib/gitlab/ci/config/external/file/artifact_spec.rb +++ b/spec/lib/gitlab/ci/config/external/file/artifact_spec.rb @@ -2,11 +2,13 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::Config::External::File::Artifact, feature_category: :pipeline_authoring do +RSpec.describe Gitlab::Ci::Config::External::File::Artifact, feature_category: :pipeline_composition do let(:parent_pipeline) { create(:ci_pipeline) } + let(:project) { parent_pipeline.project } let(:variables) {} let(:context) do - Gitlab::Ci::Config::External::Context.new(variables: variables, parent_pipeline: parent_pipeline) + Gitlab::Ci::Config::External::Context + .new(variables: variables, parent_pipeline: parent_pipeline, project: project) end let(:external_file) { described_class.new(params, context) } @@ -43,7 +45,7 @@ RSpec.describe Gitlab::Ci::Config::External::File::Artifact, feature_category: : end describe 'when used in non child pipeline context' do - let(:parent_pipeline) { nil } + let(:context) { Gitlab::Ci::Config::External::Context.new } let(:params) { { artifact: 'generated.yml' } } let(:expected_error) do @@ -201,7 +203,7 @@ RSpec.describe Gitlab::Ci::Config::External::File::Artifact, feature_category: : it { is_expected.to eq( - context_project: nil, + context_project: project.full_path, context_sha: nil, type: :artifact, location: 'generated.yml', @@ -218,7 +220,7 @@ RSpec.describe Gitlab::Ci::Config::External::File::Artifact, feature_category: : it { is_expected.to eq( - context_project: nil, + context_project: project.full_path, context_sha: nil, type: :artifact, location: 'generated.yml', @@ -227,4 +229,35 @@ RSpec.describe Gitlab::Ci::Config::External::File::Artifact, feature_category: : } end end + + describe '#to_hash' do + context 'when interpolation is being used' do + let!(:job) { create(:ci_build, name: 'generator', pipeline: parent_pipeline) } + let!(:artifacts) { create(:ci_job_artifact, :archive, job: job) } + let!(:metadata) { create(:ci_job_artifact, :metadata, job: job) } + + before do + allow_next_instance_of(Gitlab::Ci::ArtifactFileReader) do |reader| + allow(reader).to receive(:read).and_return(template) + end + end + + let(:template) do + <<~YAML + spec: + inputs: + env: + --- + deploy: + script: deploy $[[ inputs.env ]] + YAML + end + + let(:params) { { artifact: 'generated.yml', job: 'generator', inputs: { env: 'production' } } } + + it 'correctly interpolates content' do + expect(external_file.to_hash).to eq({ deploy: { script: 'deploy production' } }) + end + end + end end diff --git a/spec/lib/gitlab/ci/config/external/file/base_spec.rb b/spec/lib/gitlab/ci/config/external/file/base_spec.rb index 55d95d0c1f8..1c5918f77ca 100644 --- a/spec/lib/gitlab/ci/config/external/file/base_spec.rb +++ b/spec/lib/gitlab/ci/config/external/file/base_spec.rb @@ -2,15 +2,16 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::Config::External::File::Base, feature_category: :pipeline_authoring do +RSpec.describe Gitlab::Ci::Config::External::File::Base, feature_category: :pipeline_composition do + let_it_be(:project) { create(:project) } let(:variables) {} - let(:context_params) { { sha: 'HEAD', variables: variables } } - let(:context) { Gitlab::Ci::Config::External::Context.new(**context_params) } + let(:context_params) { { sha: 'HEAD', variables: variables, project: project } } + let(:ctx) { Gitlab::Ci::Config::External::Context.new(**context_params) } let(:test_class) do Class.new(described_class) do - def initialize(params, context) - @location = params + def initialize(params, ctx) + @location = params[:location] super end @@ -18,15 +19,18 @@ RSpec.describe Gitlab::Ci::Config::External::File::Base, feature_category: :pipe def validate_context! # no-op end + + def content + params[:content] + end end end - subject(:file) { test_class.new(location, context) } + let(:content) { 'key: value' } - before do - allow_any_instance_of(test_class) - .to receive(:content).and_return('key: value') + subject(:file) { test_class.new({ location: location, content: content }, ctx) } + before do allow_any_instance_of(Gitlab::Ci::Config::External::Context) .to receive(:check_execution_time!) end @@ -51,7 +55,7 @@ RSpec.describe Gitlab::Ci::Config::External::File::Base, feature_category: :pipe describe '#valid?' do subject(:valid?) do - Gitlab::Ci::Config::External::Mapper::Verifier.new(context).process([file]) + Gitlab::Ci::Config::External::Mapper::Verifier.new(ctx).process([file]) file.valid? end @@ -87,7 +91,12 @@ RSpec.describe Gitlab::Ci::Config::External::File::Base, feature_category: :pipe context 'when there are YAML syntax errors' do let(:location) { 'some/file/secret_file_name.yml' } - let(:variables) { Gitlab::Ci::Variables::Collection.new([{ 'key' => 'GITLAB_TOKEN', 'value' => 'secret_file_name', 'masked' => true }]) } + + let(:variables) do + Gitlab::Ci::Variables::Collection.new( + [{ 'key' => 'GITLAB_TOKEN', 'value' => 'secret_file_name', 'masked' => true }] + ) + end before do allow_any_instance_of(test_class) @@ -96,15 +105,16 @@ RSpec.describe Gitlab::Ci::Config::External::File::Base, feature_category: :pipe it 'is not a valid file' do expect(valid?).to be_falsy - expect(file.error_message).to eq('Included file `some/file/xxxxxxxxxxxxxxxx.yml` does not have valid YAML syntax!') + expect(file.error_message) + .to eq('`some/file/xxxxxxxxxxxxxxxx.yml`: content does not have a valid YAML syntax') end end context 'when the class has no validate_context!' do let(:test_class) do Class.new(described_class) do - def initialize(params, context) - @location = params + def initialize(params, ctx) + @location = params[:location] super end @@ -117,6 +127,88 @@ RSpec.describe Gitlab::Ci::Config::External::File::Base, feature_category: :pipe expect { valid? }.to raise_error(NotImplementedError) end end + + context 'when interpolation is disabled but there is a spec header' do + before do + stub_feature_flags(ci_includable_files_interpolation: false) + end + + let(:location) { 'some-location.yml' } + + let(:content) do + <<~YAML + spec: + include: + website: + --- + run: + script: deploy $[[ inputs.website ]] + YAML + end + + it 'returns an error saying that interpolation is disabled' do + expect(valid?).to be_falsy + expect(file.errors) + .to include('`some-location.yml`: can not evaluate included file because interpolation is disabled') + end + end + + context 'when interpolation was unsuccessful' do + let(:location) { 'some-location.yml' } + + context 'when context key is missing' do + let(:content) do + <<~YAML + spec: + inputs: + --- + run: + script: deploy $[[ inputs.abcd ]] + YAML + end + + it 'surfaces interpolation errors' do + expect(valid?).to be_falsy + expect(file.errors) + .to include('`some-location.yml`: interpolation interrupted by errors, unknown interpolation key: `abcd`') + end + end + + context 'when header is invalid' do + let(:content) do + <<~YAML + spec: + a: abc + --- + run: + script: deploy $[[ inputs.abcd ]] + YAML + end + + it 'surfaces header errors' do + expect(valid?).to be_falsy + expect(file.errors) + .to include('`some-location.yml`: header:spec config contains unknown keys: a') + end + end + + context 'when header is not a hash' do + let(:content) do + <<~YAML + spec: abcd + --- + run: + script: deploy $[[ inputs.abcd ]] + YAML + end + + it 'surfaces header errors' do + expect(valid?).to be_falsy + expect(file.errors) + .to contain_exactly('`some-location.yml`: header:spec config should be a hash') + end + end + end end describe '#to_hash' do @@ -142,7 +234,7 @@ RSpec.describe Gitlab::Ci::Config::External::File::Base, feature_category: :pipe it { is_expected.to eq( - context_project: nil, + context_project: project.full_path, context_sha: 'HEAD' ) } @@ -154,13 +246,13 @@ RSpec.describe Gitlab::Ci::Config::External::File::Base, feature_category: :pipe subject(:eql) { file.eql?(other_file) } context 'when the other file has the same params' do - let(:other_file) { test_class.new(location, context) } + let(:other_file) { test_class.new({ location: location, content: content }, ctx) } it { is_expected.to eq(true) } end context 'when the other file has not the same params' do - let(:other_file) { test_class.new('some/other/file', context) } + let(:other_file) { test_class.new({ location: 'some/other/file', content: content }, ctx) } it { is_expected.to eq(false) } end @@ -172,14 +264,15 @@ RSpec.describe Gitlab::Ci::Config::External::File::Base, feature_category: :pipe subject(:filehash) { file.hash } context 'with a project' do - let(:project) { create(:project) } let(:context_params) { { project: project, sha: 'HEAD', variables: variables } } - it { is_expected.to eq([location, project.full_path, 'HEAD'].hash) } + it { is_expected.to eq([{ location: location, content: content }, project.full_path, 'HEAD'].hash) } end context 'without a project' do - it { is_expected.to eq([location, nil, 'HEAD'].hash) } + let(:context_params) { { sha: 'HEAD', variables: variables } } + + it { is_expected.to eq([{ location: location, content: content }, nil, 'HEAD'].hash) } end end end diff --git a/spec/lib/gitlab/ci/config/external/file/component_spec.rb b/spec/lib/gitlab/ci/config/external/file/component_spec.rb index a162a1a8abf..fe811bce9fe 100644 --- a/spec/lib/gitlab/ci/config/external/file/component_spec.rb +++ b/spec/lib/gitlab/ci/config/external/file/component_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::Config::External::File::Component, feature_category: :pipeline_authoring do +RSpec.describe Gitlab::Ci::Config::External::File::Component, feature_category: :pipeline_composition do let_it_be(:context_project) { create(:project, :repository) } let_it_be(:project) { create(:project, :repository) } let_it_be(:user) { create(:user) } @@ -121,7 +121,7 @@ RSpec.describe Gitlab::Ci::Config::External::File::Component, feature_category: it 'is invalid' do expect(subject).to be_falsy - expect(external_resource.error_message).to match(/does not have valid YAML syntax/) + expect(external_resource.error_message).to match(/does not have a valid YAML syntax/) end end end @@ -176,4 +176,35 @@ RSpec.describe Gitlab::Ci::Config::External::File::Component, feature_category: variables: context.variables) end end + + describe '#to_hash' do + context 'when interpolation is being used' do + let(:response) do + ServiceResponse.success(payload: { content: content, path: path }) + end + + let(:path) do + instance_double(::Gitlab::Ci::Components::InstancePath, project: project, sha: '12345') + end + + let(:content) do + <<~YAML + spec: + inputs: + env: + --- + deploy: + script: deploy $[[ inputs.env ]] + YAML + end + + let(:params) do + { component: 'gitlab.com/acme/components/my-component@1.0', with: { env: 'production' } } + end + + it 'correctly interpolates the content' do + expect(external_resource.to_hash).to eq({ deploy: { script: 'deploy production' } }) + end + 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 b5895b4bc81..0643bf0c046 100644 --- a/spec/lib/gitlab/ci/config/external/file/local_spec.rb +++ b/spec/lib/gitlab/ci/config/external/file/local_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::Config::External::File::Local, feature_category: :pipeline_authoring do +RSpec.describe Gitlab::Ci::Config::External::File::Local, feature_category: :pipeline_composition do include RepoHelpers let_it_be(:project) { create(:project, :repository) } @@ -228,6 +228,34 @@ RSpec.describe Gitlab::Ci::Config::External::File::Local, feature_category: :pip expect(local_file.to_hash).to include(:rspec) end end + + context 'when interpolaton is being used' do + let(:local_file_content) do + <<~YAML + spec: + inputs: + website: + --- + test: + script: cap deploy $[[ inputs.website ]] + YAML + end + + let(:location) { '/lib/gitlab/ci/templates/existent-file.yml' } + let(:params) { { local: location, inputs: { website: 'gitlab.com' } } } + + before do + allow_any_instance_of(described_class) + .to receive(:fetch_local_content) + .and_return(local_file_content) + end + + it 'correctly interpolates the local template' do + expect(local_file).to be_valid + expect(local_file.to_hash) + .to eq({ test: { script: 'cap deploy gitlab.com' } }) + end + end end describe '#metadata' do diff --git a/spec/lib/gitlab/ci/config/external/file/project_spec.rb b/spec/lib/gitlab/ci/config/external/file/project_spec.rb index abe38cdbc3e..636241ed763 100644 --- a/spec/lib/gitlab/ci/config/external/file/project_spec.rb +++ b/spec/lib/gitlab/ci/config/external/file/project_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::Config::External::File::Project, feature_category: :pipeline_authoring do +RSpec.describe Gitlab::Ci::Config::External::File::Project, feature_category: :pipeline_composition do include RepoHelpers let_it_be(:context_project) { create(:project) } @@ -97,6 +97,36 @@ RSpec.describe Gitlab::Ci::Config::External::File::Project, feature_category: :p end end + context 'when a valid path is used in uppercase' do + let(:params) do + { project: project.full_path.upcase, file: '/file.yml' } + end + + around do |example| + create_and_delete_files(project, { '/file.yml' => 'image: image:1.0' }) do + example.run + end + end + + it { is_expected.to be_truthy } + end + + context 'when a valid different case path is used' do + let_it_be(:project) { create(:project, :repository, path: 'mY-teSt-proJect', name: 'My Test Project') } + + let(:params) do + { project: "#{project.namespace.full_path}/my-test-projecT", file: '/file.yml' } + end + + around do |example| + create_and_delete_files(project, { '/file.yml' => 'image: image:1.0' }) do + example.run + end + end + + it { is_expected.to be_truthy } + end + context 'when a valid path with custom ref is used' do let(:params) do { project: project.full_path, ref: 'master', file: '/file.yml' } @@ -230,16 +260,16 @@ RSpec.describe Gitlab::Ci::Config::External::File::Project, feature_category: :p } context 'when project name and ref include masked variables' do - let(:project_name) { 'my_project_name' } + let_it_be(:project) { create(:project, :repository, path: 'my_project_path') } + let(:branch_name) { 'merge-commit-analyze-after' } - let(:project) { create(:project, :repository, name: project_name) } let(:namespace_path) { project.namespace.full_path } let(:included_project_sha) { project.commit(branch_name).sha } let(:variables) do Gitlab::Ci::Variables::Collection.new( [ - { key: 'VAR1', value: project_name, masked: true }, + { key: 'VAR1', value: 'my_project_path', masked: true }, { key: 'VAR2', value: branch_name, masked: true } ]) end @@ -259,4 +289,37 @@ RSpec.describe Gitlab::Ci::Config::External::File::Project, feature_category: :p } end end + + describe '#to_hash' do + context 'when interpolation is being used' do + before do + project.repository.create_file( + user, + 'template-file.yml', + template, + message: 'Add template', + branch_name: 'master' + ) + end + + let(:template) do + <<~YAML + spec: + inputs: + name: + --- + rspec: + script: rspec --suite $[[ inputs.name ]] + YAML + end + + let(:params) do + { file: 'template-file.yml', ref: 'master', project: project.full_path, inputs: { name: 'abc' } } + end + + it 'correctly interpolates the content' do + expect(project_file.to_hash).to eq({ rspec: { script: 'rspec --suite abc' } }) + end + end + end end 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 27f401db76e..f8d3d1019f5 100644 --- a/spec/lib/gitlab/ci/config/external/file/remote_spec.rb +++ b/spec/lib/gitlab/ci/config/external/file/remote_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::Config::External::File::Remote, feature_category: :pipeline_authoring do +RSpec.describe Gitlab::Ci::Config::External::File::Remote, feature_category: :pipeline_composition do include StubRequests let(:variables) { Gitlab::Ci::Variables::Collection.new([{ 'key' => 'GITLAB_TOKEN', 'value' => 'secret_file', 'masked' => true }]) } @@ -234,15 +234,13 @@ RSpec.describe Gitlab::Ci::Config::External::File::Remote, feature_category: :pi end describe '#to_hash' do - subject(:to_hash) { remote_file.to_hash } - 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(to_hash).to eql( + expect(remote_file.to_hash).to eql( before_script: ["apt-get update -qq && apt-get install -y -qq sqlite3 libsqlite3-dev nodejs", "ruby -v", "which ruby", @@ -262,7 +260,7 @@ RSpec.describe Gitlab::Ci::Config::External::File::Remote, feature_category: :pi end it 'returns the content as a hash' do - expect(to_hash).to eql( + expect(remote_file.to_hash).to eql( include: [ { local: 'another-file.yml', rules: [{ exists: ['Dockerfile'] }] } @@ -270,5 +268,38 @@ RSpec.describe Gitlab::Ci::Config::External::File::Remote, feature_category: :pi ) end end + + context 'when interpolation has been used' do + let_it_be(:project) { create(:project) } + + let(:remote_file_content) do + <<~YAML + spec: + inputs: + include: + --- + include: + - local: $[[ inputs.include ]] + rules: + - exists: [Dockerfile] + YAML + end + + let(:params) { { remote: location, inputs: { include: 'some-file.yml' } } } + + let(:context_params) do + { sha: '12345', variables: variables, project: project, user: build(:user) } + end + + it 'returns the content as a hash' do + expect(remote_file).to be_valid + expect(remote_file.to_hash).to eql( + include: [ + { local: 'some-file.yml', + rules: [{ exists: ['Dockerfile'] }] } + ] + ) + end + end end end diff --git a/spec/lib/gitlab/ci/config/external/file/template_spec.rb b/spec/lib/gitlab/ci/config/external/file/template_spec.rb index 83e98874118..078b8831dc3 100644 --- a/spec/lib/gitlab/ci/config/external/file/template_spec.rb +++ b/spec/lib/gitlab/ci/config/external/file/template_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::Config::External::File::Template, feature_category: :pipeline_authoring do +RSpec.describe Gitlab::Ci::Config::External::File::Template, feature_category: :pipeline_composition do let_it_be(:project) { create(:project) } let_it_be(:user) { create(:user) } @@ -130,4 +130,37 @@ RSpec.describe Gitlab::Ci::Config::External::File::Template, feature_category: : ) } end + + describe '#to_hash' do + context 'when interpolation is being used' do + before do + allow(Gitlab::Template::GitlabCiYmlTemplate) + .to receive(:find) + .and_return(template_double) + end + + let(:template_double) do + instance_double(Gitlab::Template::GitlabCiYmlTemplate, content: template_content) + end + + let(:template_content) do + <<~YAML + spec: + inputs: + env: + --- + deploy: + script: deploy $[[ inputs.env ]] + YAML + end + + let(:params) do + { template: template, inputs: { env: 'production' } } + end + + it 'correctly interpolates the content' do + expect(template_file.to_hash).to eq({ deploy: { script: 'deploy production' } }) + end + end + end end diff --git a/spec/lib/gitlab/ci/config/external/interpolator_spec.rb b/spec/lib/gitlab/ci/config/external/interpolator_spec.rb new file mode 100644 index 00000000000..fe6f97a66a5 --- /dev/null +++ b/spec/lib/gitlab/ci/config/external/interpolator_spec.rb @@ -0,0 +1,319 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Config::External::Interpolator, feature_category: :pipeline_composition do + let_it_be(:project) { create(:project) } + + let(:ctx) { instance_double(Gitlab::Ci::Config::External::Context, project: project, user: build(:user, id: 1234)) } + let(:result) { ::Gitlab::Ci::Config::Yaml::Result.new(config: [header, content]) } + + subject { described_class.new(result, arguments, ctx) } + + 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 + subject.interpolate! + + expect(subject).to be_valid + expect(subject.to_hash).to eq({ test: 'deploy gitlab.com' }) + end + + it 'tracks the event' do + expect(::Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:track_event) + .with('ci_interpolation_users', { values: 1234 }) + + subject.interpolate! + end + end + + context 'when config has a syntax error' do + let(:result) { ::Gitlab::Ci::Config::Yaml::Result.new(error: ArgumentError.new) } + + let(:arguments) do + { website: 'gitlab.com' } + end + + it 'surfaces an error about invalid config' do + subject.interpolate! + + expect(subject).not_to be_valid + expect(subject.error_message).to eq subject.errors.first + expect(subject.errors).to include 'content does not have a valid YAML syntax' + 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 + subject.interpolate! + + expect(subject).not_to be_valid + expect(subject.error_message).to eq subject.errors.first + expect(subject.errors).to include('header:spec config contains unknown keys: arguments') + 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 'correctly interpolates the config' do + subject.interpolate! + + expect(subject).not_to be_valid + expect(subject.errors).to include 'unknown interpolation key: `abc`' + expect(subject.error_message).to eq 'interpolation interrupted by errors, unknown interpolation key: `abc`' + 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 'correctly interpolates the config' do + subject.interpolate! + + expect(subject).not_to be_valid + expect(subject.error_message).to eq subject.errors.first + expect(subject.errors).to include 'unsupported value in input argument `website`' + 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 'correctly interpolates the config' do + subject.interpolate! + + expect(subject).not_to be_valid + expect(subject.error_message).to eq 'interpolation interrupted by errors, unknown interpolation key: `something`' + end + end + + describe '#to_hash' do + context 'when interpolation is disabled' do + before do + stub_feature_flags(ci_includable_files_interpolation: false) + end + + let(:header) do + { spec: { inputs: { website: nil } } } + end + + let(:content) do + { test: 'deploy $[[ inputs.website ]]' } + end + + let(:arguments) { {} } + + it 'returns an empty hash' do + subject.interpolate! + + expect(subject.to_hash).to be_empty + end + end + + 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 + subject.interpolate! + + expect(subject.to_hash).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 + subject.interpolate! + + expect(subject.to_hash).to eq({ test: 'deploy gitlab.com' }) + end + end + end + + describe '#ready?' 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 false if interpolation has not been done yet' do + expect(subject).not_to be_ready + end + + it 'returns true if interpolation has been performed' do + subject.interpolate! + + expect(subject).to be_ready + end + + context 'when interpolation can not be performed' do + let(:result) do + ::Gitlab::Ci::Config::Yaml::Result.new(error: ArgumentError.new) + end + + it 'returns true if interpolator has preliminary errors' do + expect(subject).to be_ready + end + + it 'returns true if interpolation has been attempted' do + subject.interpolate! + + expect(subject).to be_ready + end + end + end + + describe '#interpolate?' 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 + + context 'when interpolation can be performed' do + it 'will perform interpolation' do + expect(subject.interpolate?).to eq true + end + end + + context 'when interpolation is disabled' do + before do + stub_feature_flags(ci_includable_files_interpolation: false) + end + + it 'will not perform interpolation' do + expect(subject.interpolate?).to eq false + end + end + + context 'when an interpolation header is missing' do + let(:header) { nil } + + it 'will not perform interpolation' do + expect(subject.interpolate?).to eq false + end + end + + context 'when interpolator has preliminary errors' do + let(:result) do + ::Gitlab::Ci::Config::Yaml::Result.new(error: ArgumentError.new) + end + + it 'will not perform interpolation' do + expect(subject.interpolate?).to eq false + end + end + end + + describe '#has_header?' do + let(:content) do + { test: 'deploy $[[ inputs.something.abc ]] $[[ inputs.cde ]] $[[ efg ]]' } + end + + let(:arguments) do + { website: 'gitlab.com' } + end + + context 'when header is an empty hash' do + let(:header) { {} } + + it 'does not have a header available' do + expect(subject).not_to have_header + end + end + + context 'when header is not specified' do + let(:header) { nil } + + it 'does not have a header available' do + expect(subject).not_to have_header + end + end + end +end diff --git a/spec/lib/gitlab/ci/config/external/mapper/base_spec.rb b/spec/lib/gitlab/ci/config/external/mapper/base_spec.rb index 0fdcc5e8ff7..ce8f3756cbc 100644 --- a/spec/lib/gitlab/ci/config/external/mapper/base_spec.rb +++ b/spec/lib/gitlab/ci/config/external/mapper/base_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::Config::External::Mapper::Base, feature_category: :pipeline_authoring do +RSpec.describe Gitlab::Ci::Config::External::Mapper::Base, feature_category: :pipeline_composition do let(:test_class) do Class.new(described_class) do def self.name diff --git a/spec/lib/gitlab/ci/config/external/mapper/filter_spec.rb b/spec/lib/gitlab/ci/config/external/mapper/filter_spec.rb index df2a2f0fd01..5195567ebb4 100644 --- a/spec/lib/gitlab/ci/config/external/mapper/filter_spec.rb +++ b/spec/lib/gitlab/ci/config/external/mapper/filter_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::Config::External::Mapper::Filter, feature_category: :pipeline_authoring do +RSpec.describe Gitlab::Ci::Config::External::Mapper::Filter, feature_category: :pipeline_composition do let_it_be(:variables) do Gitlab::Ci::Variables::Collection.new.tap do |variables| variables.append(key: 'VARIABLE1', value: 'hello') diff --git a/spec/lib/gitlab/ci/config/external/mapper/location_expander_spec.rb b/spec/lib/gitlab/ci/config/external/mapper/location_expander_spec.rb index b14b6b0ca29..1e490bf1d16 100644 --- a/spec/lib/gitlab/ci/config/external/mapper/location_expander_spec.rb +++ b/spec/lib/gitlab/ci/config/external/mapper/location_expander_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::Config::External::Mapper::LocationExpander, feature_category: :pipeline_authoring do +RSpec.describe Gitlab::Ci::Config::External::Mapper::LocationExpander, feature_category: :pipeline_composition do include RepoHelpers let_it_be(:project) { create(:project, :repository) } diff --git a/spec/lib/gitlab/ci/config/external/mapper/matcher_spec.rb b/spec/lib/gitlab/ci/config/external/mapper/matcher_spec.rb index 11c79e19cff..719c75dca80 100644 --- a/spec/lib/gitlab/ci/config/external/mapper/matcher_spec.rb +++ b/spec/lib/gitlab/ci/config/external/mapper/matcher_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::Config::External::Mapper::Matcher, feature_category: :pipeline_authoring do +RSpec.describe Gitlab::Ci::Config::External::Mapper::Matcher, feature_category: :pipeline_composition do let_it_be(:variables) do Gitlab::Ci::Variables::Collection.new.tap do |variables| variables.append(key: 'A_MASKED_VAR', value: 'this-is-secret', masked: true) @@ -16,28 +16,56 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper::Matcher, feature_category: subject(:matcher) { described_class.new(context) } describe '#process' do - let(:locations) do - [ - { local: 'file.yml' }, - { file: 'file.yml', project: 'namespace/project' }, - { component: 'gitlab.com/org/component@1.0' }, - { remote: 'https://example.com/.gitlab-ci.yml' }, - { template: 'file.yml' }, - { artifact: 'generated.yml', job: 'test' } - ] + subject(:process) { matcher.process(locations) } + + context 'with ci_include_components FF disabled' do + before do + stub_feature_flags(ci_include_components: false) + end + + let(:locations) do + [ + { local: 'file.yml' }, + { file: 'file.yml', project: 'namespace/project' }, + { remote: 'https://example.com/.gitlab-ci.yml' }, + { template: 'file.yml' }, + { artifact: 'generated.yml', job: 'test' } + ] + end + + it 'returns an array of file objects' do + is_expected.to contain_exactly( + an_instance_of(Gitlab::Ci::Config::External::File::Local), + an_instance_of(Gitlab::Ci::Config::External::File::Project), + an_instance_of(Gitlab::Ci::Config::External::File::Remote), + an_instance_of(Gitlab::Ci::Config::External::File::Template), + an_instance_of(Gitlab::Ci::Config::External::File::Artifact) + ) + end end - subject(:process) { matcher.process(locations) } + context 'with ci_include_components FF enabled' do + let(:locations) do + [ + { local: 'file.yml' }, + { file: 'file.yml', project: 'namespace/project' }, + { component: 'gitlab.com/org/component@1.0' }, + { remote: 'https://example.com/.gitlab-ci.yml' }, + { template: 'file.yml' }, + { artifact: 'generated.yml', job: 'test' } + ] + end - it 'returns an array of file objects' do - is_expected.to contain_exactly( - an_instance_of(Gitlab::Ci::Config::External::File::Local), - an_instance_of(Gitlab::Ci::Config::External::File::Project), - an_instance_of(Gitlab::Ci::Config::External::File::Component), - an_instance_of(Gitlab::Ci::Config::External::File::Remote), - an_instance_of(Gitlab::Ci::Config::External::File::Template), - an_instance_of(Gitlab::Ci::Config::External::File::Artifact) - ) + it 'returns an array of file objects' do + is_expected.to contain_exactly( + an_instance_of(Gitlab::Ci::Config::External::File::Local), + an_instance_of(Gitlab::Ci::Config::External::File::Project), + an_instance_of(Gitlab::Ci::Config::External::File::Component), + an_instance_of(Gitlab::Ci::Config::External::File::Remote), + an_instance_of(Gitlab::Ci::Config::External::File::Template), + an_instance_of(Gitlab::Ci::Config::External::File::Artifact) + ) + end end context 'when a location is not valid' do diff --git a/spec/lib/gitlab/ci/config/external/mapper/normalizer_spec.rb b/spec/lib/gitlab/ci/config/external/mapper/normalizer_spec.rb index 709c234253b..09212833d84 100644 --- a/spec/lib/gitlab/ci/config/external/mapper/normalizer_spec.rb +++ b/spec/lib/gitlab/ci/config/external/mapper/normalizer_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::Config::External::Mapper::Normalizer, feature_category: :pipeline_authoring do +RSpec.describe Gitlab::Ci::Config::External::Mapper::Normalizer, feature_category: :pipeline_composition do let_it_be(:variables) do Gitlab::Ci::Variables::Collection.new.tap do |variables| variables.append(key: 'VARIABLE1', value: 'config') diff --git a/spec/lib/gitlab/ci/config/external/mapper/variables_expander_spec.rb b/spec/lib/gitlab/ci/config/external/mapper/variables_expander_spec.rb index f7454dcd4be..5def516bb1e 100644 --- a/spec/lib/gitlab/ci/config/external/mapper/variables_expander_spec.rb +++ b/spec/lib/gitlab/ci/config/external/mapper/variables_expander_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::Config::External::Mapper::VariablesExpander, feature_category: :pipeline_authoring do +RSpec.describe Gitlab::Ci::Config::External::Mapper::VariablesExpander, feature_category: :secrets_management do let_it_be(:variables) do Gitlab::Ci::Variables::Collection.new.tap do |variables| variables.append(key: 'VARIABLE1', value: 'hello') diff --git a/spec/lib/gitlab/ci/config/external/mapper/verifier_spec.rb b/spec/lib/gitlab/ci/config/external/mapper/verifier_spec.rb index a219666f24e..1ee46daa196 100644 --- a/spec/lib/gitlab/ci/config/external/mapper/verifier_spec.rb +++ b/spec/lib/gitlab/ci/config/external/mapper/verifier_spec.rb @@ -2,11 +2,11 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::Config::External::Mapper::Verifier, feature_category: :pipeline_authoring do +RSpec.describe Gitlab::Ci::Config::External::Mapper::Verifier, feature_category: :pipeline_composition do include RepoHelpers include StubRequests - let_it_be(:project) { create(:project, :repository) } + let_it_be(:project) { create(:project, :small_repo) } let_it_be(:user) { project.owner } let(:context) do @@ -38,7 +38,7 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper::Verifier, feature_category: } end - around(:all) do |example| + around do |example| create_and_delete_files(project, project_files) do example.run end @@ -84,42 +84,140 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper::Verifier, feature_category: end context 'when files are project files' do - let_it_be(:included_project) { create(:project, :repository, namespace: project.namespace, creator: user) } + let_it_be(:included_project1) { create(:project, :small_repo, namespace: project.namespace, creator: user) } + let_it_be(:included_project2) { create(:project, :small_repo, namespace: project.namespace, creator: user) } let(:files) do [ Gitlab::Ci::Config::External::File::Project.new( - { file: 'myfolder/file1.yml', project: included_project.full_path }, context + { file: 'myfolder/file1.yml', project: included_project1.full_path }, context ), Gitlab::Ci::Config::External::File::Project.new( - { file: 'myfolder/file2.yml', project: included_project.full_path }, context + { file: 'myfolder/file2.yml', project: included_project1.full_path }, context ), Gitlab::Ci::Config::External::File::Project.new( - { file: 'myfolder/file3.yml', project: included_project.full_path }, context + { file: 'myfolder/file3.yml', project: included_project1.full_path, ref: 'master' }, context + ), + Gitlab::Ci::Config::External::File::Project.new( + { file: 'myfolder/file1.yml', project: included_project2.full_path }, context + ), + Gitlab::Ci::Config::External::File::Project.new( + { file: 'myfolder/file2.yml', project: included_project2.full_path }, context ) ] end - around(:all) do |example| - create_and_delete_files(included_project, project_files) do - example.run + around do |example| + create_and_delete_files(included_project1, project_files) do + create_and_delete_files(included_project2, project_files) do + example.run + end end end - it 'returns an array of file objects' do + it 'returns an array of valid file objects' do expect(process.map(&:location)).to contain_exactly( - 'myfolder/file1.yml', 'myfolder/file2.yml', 'myfolder/file3.yml' + 'myfolder/file1.yml', 'myfolder/file2.yml', 'myfolder/file3.yml', 'myfolder/file1.yml', 'myfolder/file2.yml' ) + + expect(process.all?(&:valid?)).to be_truthy end it 'adds files to the expandset' do - expect { process }.to change { context.expandset.count }.by(3) + expect { process }.to change { context.expandset.count }.by(5) end it 'calls Gitaly only once for all files', :request_store do - # 1 for project.commit.id, 3 for the sha check, 1 for the files + files # calling this to load project creations and the `project.commit.id` call + + # 3 for the sha check, 2 for the files in batch expect { process }.to change { Gitlab::GitalyClient.get_request_count }.by(5) end + + it 'queries with batch', :use_sql_query_cache do + files # calling this to load project creations and the `project.commit.id` call + + queries = ActiveRecord::QueryRecorder.new(skip_cached: false) { process } + projects_queries = queries.occurrences_starting_with('SELECT "projects"') + access_check_queries = queries.occurrences_starting_with('SELECT MAX("project_authorizations"."access_level")') + + # We could not reduce the number of projects queries because we need to call project for + # the `can_access_local_content?` and `sha` BatchLoaders. + expect(projects_queries.values.sum).to eq(2) + expect(access_check_queries.values.sum).to eq(2) + end + + context 'when the FF ci_batch_project_includes_context is disabled' do + before do + stub_feature_flags(ci_batch_project_includes_context: false) + end + + it 'returns an array of file objects' do + expect(process.map(&:location)).to contain_exactly( + 'myfolder/file1.yml', 'myfolder/file2.yml', 'myfolder/file3.yml', + 'myfolder/file1.yml', 'myfolder/file2.yml' + ) + end + + it 'adds files to the expandset' do + expect { process }.to change { context.expandset.count }.by(5) + end + + it 'calls Gitaly for all files', :request_store do + files # calling this to load project creations and the `project.commit.id` call + + # 5 for the sha check, 2 for the files in batch + expect { process }.to change { Gitlab::GitalyClient.get_request_count }.by(7) + end + + it 'queries without batch', :use_sql_query_cache do + files # calling this to load project creations and the `project.commit.id` call + + queries = ActiveRecord::QueryRecorder.new(skip_cached: false) { process } + projects_queries = queries.occurrences_starting_with('SELECT "projects"') + access_check_queries = queries.occurrences_starting_with( + 'SELECT MAX("project_authorizations"."access_level")' + ) + + expect(projects_queries.values.sum).to eq(5) + expect(access_check_queries.values.sum).to eq(5) + end + end + + context 'when a project is missing' do + let(:files) do + [ + Gitlab::Ci::Config::External::File::Project.new( + { file: 'myfolder/file1.yml', project: included_project1.full_path }, context + ), + Gitlab::Ci::Config::External::File::Project.new( + { file: 'myfolder/file2.yml', project: 'invalid-project' }, context + ) + ] + end + + it 'returns an array of file objects' do + expect(process.map(&:location)).to contain_exactly( + 'myfolder/file1.yml', 'myfolder/file2.yml' + ) + + expect(process.all?(&:valid?)).to be_falsey + end + + context 'when the FF ci_batch_project_includes_context is disabled' do + before do + stub_feature_flags(ci_batch_project_includes_context: false) + end + + it 'returns an array of file objects' do + expect(process.map(&:location)).to contain_exactly( + 'myfolder/file1.yml', 'myfolder/file2.yml' + ) + + expect(process.all?(&:valid?)).to be_falsey + end + end + end end context 'when a file includes other files' do @@ -150,7 +248,30 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper::Verifier, feature_category: end end - context 'when total file count exceeds max_includes' do + describe 'max includes detection' do + shared_examples 'verifies max includes' do + context 'when total file count is equal to max_includes' do + before do + allow(context).to receive(:max_includes).and_return(expected_total_file_count) + end + + it 'adds the expected number of files to expandset' do + expect { process }.not_to raise_error + expect(context.expandset.count).to eq(expected_total_file_count) + end + end + + context 'when total file count exceeds max_includes' do + before do + allow(context).to receive(:max_includes).and_return(expected_total_file_count - 1) + end + + it 'raises error' do + expect { process }.to raise_error(expected_error_class) + end + end + end + context 'when files are nested' do let(:files) do [ @@ -158,9 +279,21 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper::Verifier, feature_category: ] end - it 'raises Processor::IncludeError' do - allow(context).to receive(:max_includes).and_return(1) - expect { process }.to raise_error(Gitlab::Ci::Config::External::Processor::IncludeError) + let(:expected_total_file_count) { 4 } # Includes nested_configs.yml + 3 nested files + let(:expected_error_class) { Gitlab::Ci::Config::External::Processor::IncludeError } + + it_behaves_like 'verifies max includes' + + context 'when duplicate files are included' do + let(:expected_total_file_count) { 8 } # 2 x (Includes nested_configs.yml + 3 nested files) + let(:files) do + [ + Gitlab::Ci::Config::External::File::Local.new({ local: 'nested_configs.yml' }, context), + Gitlab::Ci::Config::External::File::Local.new({ local: 'nested_configs.yml' }, context) + ] + end + + it_behaves_like 'verifies max includes' end end @@ -172,34 +305,112 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper::Verifier, feature_category: ] end - it 'raises Mapper::TooManyIncludesError' do - allow(context).to receive(:max_includes).and_return(1) - expect { process }.to raise_error(Gitlab::Ci::Config::External::Mapper::TooManyIncludesError) + let(:expected_total_file_count) { files.count } + let(:expected_error_class) { Gitlab::Ci::Config::External::Mapper::TooManyIncludesError } + + it_behaves_like 'verifies max includes' + + context 'when duplicate files are included' do + let(:files) do + [ + Gitlab::Ci::Config::External::File::Local.new({ local: 'myfolder/file1.yml' }, context), + Gitlab::Ci::Config::External::File::Local.new({ local: 'myfolder/file2.yml' }, context), + Gitlab::Ci::Config::External::File::Local.new({ local: 'myfolder/file2.yml' }, context) + ] + end + + let(:expected_total_file_count) { files.count } + + it_behaves_like 'verifies max includes' end end - context 'when files are duplicates' do + context 'when there is a circular include' do + let(:project_files) do + { + 'myfolder/file1.yml' => <<~YAML + include: myfolder/file1.yml + YAML + } + end + let(:files) do [ - Gitlab::Ci::Config::External::File::Local.new({ local: 'myfolder/file1.yml' }, context), - Gitlab::Ci::Config::External::File::Local.new({ local: 'myfolder/file1.yml' }, context), Gitlab::Ci::Config::External::File::Local.new({ local: 'myfolder/file1.yml' }, context) ] end + before do + allow(context).to receive(:max_includes).and_return(10) + end + it 'raises error' do - allow(context).to receive(:max_includes).and_return(2) - expect { process }.to raise_error(Gitlab::Ci::Config::External::Mapper::TooManyIncludesError) + expect { process }.to raise_error(Gitlab::Ci::Config::External::Processor::IncludeError) end + end - context 'when FF ci_includes_count_duplicates is disabled' do - before do - stub_feature_flags(ci_includes_count_duplicates: false) - end + context 'when a file is an internal include' do + let(:project_files) do + { + 'myfolder/file1.yml' => <<~YAML, + my_build: + script: echo Hello World + YAML + '.internal-include.yml' => <<~YAML + include: + - local: myfolder/file1.yml + YAML + } + end + + let(:files) do + [ + Gitlab::Ci::Config::External::File::Local.new({ local: '.internal-include.yml' }, context) + ] + end - it 'does not raise error' do - allow(context).to receive(:max_includes).and_return(2) + let(:total_file_count) { 2 } # Includes .internal-include.yml + myfolder/file1.yml + let(:pipeline_config) { instance_double(Gitlab::Ci::ProjectConfig) } + + let(:context) do + Gitlab::Ci::Config::External::Context.new( + project: project, + user: user, + sha: project.commit.id, + pipeline_config: pipeline_config + ) + end + + before do + allow(pipeline_config).to receive(:internal_include_prepended?).and_return(true) + allow(context).to receive(:max_includes).and_return(1) + end + + context 'when total file count excluding internal include is equal to max_includes' do + it 'does not add the internal include to expandset' do expect { process }.not_to raise_error + expect(context.expandset.count).to eq(total_file_count - 1) + expect(context.expandset.first.location).to eq('myfolder/file1.yml') + end + end + + context 'when total file count excluding internal include exceeds max_includes' do + let(:project_files) do + { + 'myfolder/file1.yml' => <<~YAML, + my_build: + script: echo Hello World + YAML + '.internal-include.yml' => <<~YAML + include: + - local: myfolder/file1.yml + - local: myfolder/file1.yml + YAML + } + end + + it 'raises error' do + expect { process }.to raise_error(Gitlab::Ci::Config::External::Processor::IncludeError) end end end diff --git a/spec/lib/gitlab/ci/config/external/mapper_spec.rb b/spec/lib/gitlab/ci/config/external/mapper_spec.rb index b3115617084..56d1ddee4b8 100644 --- a/spec/lib/gitlab/ci/config/external/mapper_spec.rb +++ b/spec/lib/gitlab/ci/config/external/mapper_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::Config::External::Mapper, feature_category: :pipeline_authoring do +RSpec.describe Gitlab::Ci::Config::External::Mapper, feature_category: :pipeline_composition do include StubRequests include RepoHelpers @@ -234,17 +234,6 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper, feature_category: :pipeline process expect(context.expandset.size).to eq(2) end - - context 'when FF ci_includes_count_duplicates is disabled' do - before do - stub_feature_flags(ci_includes_count_duplicates: false) - end - - it 'has expanset with one' do - process - expect(context.expandset.size).to eq(1) - end - end end context 'when passing max number of files' do diff --git a/spec/lib/gitlab/ci/config/external/processor_spec.rb b/spec/lib/gitlab/ci/config/external/processor_spec.rb index bb65c2ef10c..74afb3b1e97 100644 --- a/spec/lib/gitlab/ci/config/external/processor_spec.rb +++ b/spec/lib/gitlab/ci/config/external/processor_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::Config::External::Processor, feature_category: :pipeline_authoring do +RSpec.describe Gitlab::Ci::Config::External::Processor, feature_category: :pipeline_composition do include StubRequests include RepoHelpers @@ -221,7 +221,7 @@ RSpec.describe Gitlab::Ci::Config::External::Processor, feature_category: :pipel it 'raises an error' do expect { processor.perform }.to raise_error( described_class::IncludeError, - "Included file `lib/gitlab/ci/templates/template.yml` does not have valid YAML syntax!" + '`lib/gitlab/ci/templates/template.yml`: content does not have a valid YAML syntax' ) end end diff --git a/spec/lib/gitlab/ci/config/external/rules_spec.rb b/spec/lib/gitlab/ci/config/external/rules_spec.rb index 227b62d8ce8..cc73338b5a8 100644 --- a/spec/lib/gitlab/ci/config/external/rules_spec.rb +++ b/spec/lib/gitlab/ci/config/external/rules_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::Config::External::Rules, feature_category: :pipeline_authoring do +RSpec.describe Gitlab::Ci::Config::External::Rules, feature_category: :pipeline_composition do let(:rule_hashes) {} subject(:rules) { described_class.new(rule_hashes) } diff --git a/spec/lib/gitlab/ci/config/header/input_spec.rb b/spec/lib/gitlab/ci/config/header/input_spec.rb new file mode 100644 index 00000000000..73b5b8f9497 --- /dev/null +++ b/spec/lib/gitlab/ci/config/header/input_spec.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Config::Header::Input, feature_category: :pipeline_composition do + let(:factory) do + Gitlab::Config::Entry::Factory + .new(described_class) + .value(input_hash) + .with(key: input_name) + end + + let(:input_name) { 'foo' } + + subject(:config) { factory.create!.tap(&:compose!) } + + shared_examples 'a valid input' do + let(:expected_hash) { input_hash } + + it 'passes validations' do + expect(config).to be_valid + expect(config.errors).to be_empty + end + + it 'returns the value' do + expect(config.value).to eq(expected_hash) + end + end + + shared_examples 'an invalid input' do + let(:expected_hash) { input_hash } + + it 'fails validations' do + expect(config).not_to be_valid + expect(config.errors).to eq(expected_errors) + end + + it 'returns the value' do + expect(config.value).to eq(expected_hash) + end + end + + context 'when has a default value' do + let(:input_hash) { { default: 'bar' } } + + it_behaves_like 'a valid input' + end + + context 'when is a required required input' do + let(:input_hash) { nil } + + it_behaves_like 'a valid input' + end + + context 'when contains unknown keywords' do + let(:input_hash) { { test: 123 } } + let(:expected_errors) { ['foo config contains unknown keys: test'] } + + it_behaves_like 'an invalid input' + end + + context 'when has invalid name' do + let(:input_name) { [123] } + let(:input_hash) { {} } + + let(:expected_errors) { ['123 key must be an alphanumeric string'] } + + it_behaves_like 'an invalid input' + end +end diff --git a/spec/lib/gitlab/ci/config/header/root_spec.rb b/spec/lib/gitlab/ci/config/header/root_spec.rb new file mode 100644 index 00000000000..55f77137619 --- /dev/null +++ b/spec/lib/gitlab/ci/config/header/root_spec.rb @@ -0,0 +1,133 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Config::Header::Root, feature_category: :pipeline_composition do + let(:factory) { Gitlab::Config::Entry::Factory.new(described_class).value(header_hash) } + + subject(:config) { factory.create!.tap(&:compose!) } + + shared_examples 'a valid header' do + let(:expected_hash) { header_hash } + + it 'passes validations' do + expect(config).to be_valid + expect(config.errors).to be_empty + end + + it 'returns the value' do + expect(config.value).to eq(expected_hash) + end + end + + shared_examples 'an invalid header' do + let(:expected_hash) { header_hash } + + it 'fails validations' do + expect(config).not_to be_valid + expect(config.errors).to eq(expected_errors) + end + + it 'returns the value' do + expect(config.value).to eq(expected_hash) + end + end + + context 'when header contains default and required values for inputs' do + let(:header_hash) do + { + spec: { + inputs: { + test: {}, + foo: { + default: 'bar' + } + } + } + } + end + + it_behaves_like 'a valid header' + end + + context 'when header contains minimal data' do + let(:header_hash) do + { + spec: { + inputs: nil + } + } + end + + it_behaves_like 'a valid header' do + let(:expected_hash) { { spec: {} } } + end + end + + context 'when header contains required inputs' do + let(:header_hash) do + { + spec: { + inputs: { foo: nil } + } + } + end + + it_behaves_like 'a valid header' do + let(:expected_hash) do + { + spec: { + inputs: { foo: {} } + } + } + end + end + end + + context 'when header contains unknown keywords' do + let(:header_hash) { { test: 123 } } + let(:expected_errors) { ['root config contains unknown keys: test'] } + + it_behaves_like 'an invalid header' + end + + context 'when header input entry has an unknown key' do + let(:header_hash) do + { + spec: { + inputs: { + foo: { + bad: 'value' + } + } + } + } + end + + let(:expected_errors) { ['spec:inputs:foo config contains unknown keys: bad'] } + + it_behaves_like 'an invalid header' + end + + describe '#inputs_value' do + let(:header_hash) do + { + spec: { + inputs: { + foo: nil, + bar: { + default: 'baz' + } + } + } + } + end + + it 'returns the inputs' do + expect(config.inputs_value).to eq({ + foo: {}, + bar: { default: 'baz' } + }) + end + end +end diff --git a/spec/lib/gitlab/ci/config/header/spec_spec.rb b/spec/lib/gitlab/ci/config/header/spec_spec.rb new file mode 100644 index 00000000000..74cfb39dfd5 --- /dev/null +++ b/spec/lib/gitlab/ci/config/header/spec_spec.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Config::Header::Spec, feature_category: :pipeline_composition do + let(:factory) { Gitlab::Config::Entry::Factory.new(described_class).value(spec_hash) } + + subject(:config) { factory.create!.tap(&:compose!) } + + context 'when spec contains default values for inputs' do + let(:spec_hash) do + { + inputs: { + foo: { + default: 'bar' + } + } + } + end + + it 'passes validations' do + expect(config).to be_valid + expect(config.errors).to be_empty + end + + it 'returns the value' do + expect(config.value).to eq(spec_hash) + end + end + + context 'when spec contains a required value' do + let(:spec_hash) do + { inputs: { foo: nil } } + end + + it 'parses the config correctly' do + expect(config).to be_valid + expect(config.errors).to be_empty + expect(config.value).to eq({ inputs: { foo: {} } }) + end + end + + context 'when spec contains unknown keywords' do + let(:spec_hash) { { test: 123 } } + let(:expected_errors) { ['spec config contains unknown keys: test'] } + + it 'fails validations' do + expect(config).not_to be_valid + expect(config.errors).to eq(expected_errors) + end + + it 'returns the value' do + expect(config.value).to eq(spec_hash) + end + end +end diff --git a/spec/lib/gitlab/ci/config/normalizer/number_strategy_spec.rb b/spec/lib/gitlab/ci/config/normalizer/number_strategy_spec.rb index 06f47fe11c6..965963d40cd 100644 --- a/spec/lib/gitlab/ci/config/normalizer/number_strategy_spec.rb +++ b/spec/lib/gitlab/ci/config/normalizer/number_strategy_spec.rb @@ -53,6 +53,22 @@ RSpec.describe Gitlab::Ci::Config::Normalizer::NumberStrategy do end end + shared_examples 'single parallelized job' do + it { expect(subject.size).to eq(1) } + + it 'has attributes' do + expect(subject.map(&:attributes)).to match_array( + [ + { name: 'test 1/1', instance: 1, parallel: { total: 1 } } + ] + ) + end + + it 'has parallelized name' do + expect(subject.map(&:name)).to match_array(['test 1/1']) + end + end + context 'with numbers' do let(:config) { 3 } @@ -64,5 +80,11 @@ RSpec.describe Gitlab::Ci::Config::Normalizer::NumberStrategy do it_behaves_like 'parallelized job' end + + context 'with one' do + let(:config) { 1 } + + it_behaves_like 'single parallelized job' + end end end diff --git a/spec/lib/gitlab/ci/config/yaml/result_spec.rb b/spec/lib/gitlab/ci/config/yaml/result_spec.rb new file mode 100644 index 00000000000..72d96349668 --- /dev/null +++ b/spec/lib/gitlab/ci/config/yaml/result_spec.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Config::Yaml::Result, feature_category: :pipeline_composition do + it 'does not have a header when config is a single hash' do + result = described_class.new(config: { a: 1, b: 2 }) + + expect(result).not_to have_header + end + + context 'when config is an array of hashes' do + context 'when first document matches the header schema' do + it 'has a header' do + result = described_class.new(config: [{ spec: { inputs: {} } }, { b: 2 }]) + + expect(result).to have_header + expect(result.header).to eq({ spec: { inputs: {} } }) + expect(result.content).to eq({ b: 2 }) + end + end + + context 'when first document does not match the header schema' do + it 'does not have header' do + result = described_class.new(config: [{ a: 1 }, { b: 2 }]) + + expect(result).not_to have_header + expect(result.content).to eq({ a: 1 }) + end + end + end + + context 'when the first document is undefined' do + it 'does not have header' do + result = described_class.new(config: [nil, { a: 1 }]) + + expect(result).not_to have_header + expect(result.content).to be_nil + end + end + + it 'raises an error when reading a header when there is none' do + result = described_class.new(config: { b: 2 }) + + expect { result.header }.to raise_error(ArgumentError) + end + + it 'stores an error / exception when initialized with it' do + result = described_class.new(error: ArgumentError.new('abc')) + + expect(result).not_to be_valid + expect(result.error).to be_a ArgumentError + end +end diff --git a/spec/lib/gitlab/ci/config/yaml_spec.rb b/spec/lib/gitlab/ci/config/yaml_spec.rb index 4b34553f55e..beb872071d2 100644 --- a/spec/lib/gitlab/ci/config/yaml_spec.rb +++ b/spec/lib/gitlab/ci/config/yaml_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::Config::Yaml, feature_category: :pipeline_authoring do +RSpec.describe Gitlab::Ci::Config::Yaml, feature_category: :pipeline_composition do describe '.load!' do it 'loads a single-doc YAML file' do yaml = <<~YAML @@ -50,6 +50,15 @@ RSpec.describe Gitlab::Ci::Config::Yaml, feature_category: :pipeline_authoring d }) end + context 'when YAML is invalid' do + let(:yaml) { 'some: invalid: syntax' } + + it 'raises an error' do + expect { described_class.load!(yaml) } + .to raise_error ::Gitlab::Config::Loader::FormatError, /mapping values are not allowed in this context/ + end + end + context 'when ci_multi_doc_yaml is disabled' do before do stub_feature_flags(ci_multi_doc_yaml: false) @@ -102,4 +111,152 @@ RSpec.describe Gitlab::Ci::Config::Yaml, feature_category: :pipeline_authoring d end end end + + describe '.load_result!' do + let_it_be(:project) { create(:project) } + + subject(:result) { described_class.load_result!(yaml, project: project) } + + context 'when syntax is invalid' do + let(:yaml) { 'some: invalid: syntax' } + + it 'returns an invalid result object' do + expect(result).not_to be_valid + expect(result.error).to be_a ::Gitlab::Config::Loader::FormatError + end + end + + context 'when the first document is a header' do + context 'with explicit document start marker' do + let(:yaml) do + <<~YAML + --- + spec: + --- + b: 2 + YAML + end + + it 'considers the first document as header and the second as content' do + expect(result).to be_valid + expect(result.error).to be_nil + expect(result.header).to eq({ spec: nil }) + expect(result.content).to eq({ b: 2 }) + end + end + end + + context 'when first document is empty' do + let(:yaml) do + <<~YAML + --- + --- + b: 2 + YAML + end + + it 'considers the first document as header and the second as content' do + expect(result).not_to have_header + end + end + + context 'when first document is an empty hash' do + let(:yaml) do + <<~YAML + {} + --- + b: 2 + YAML + end + + it 'returns second document as a content' do + expect(result).not_to have_header + expect(result.content).to eq({ b: 2 }) + end + end + + context 'when first an array' do + let(:yaml) do + <<~YAML + --- + - a + - b + --- + b: 2 + YAML + end + + it 'considers the first document as header and the second as content' do + expect(result).not_to have_header + end + end + + context 'when the first document is not a header' do + let(:yaml) do + <<~YAML + a: 1 + --- + b: 2 + YAML + end + + it 'considers the first document as content for backwards compatibility' do + expect(result).to be_valid + expect(result.error).to be_nil + expect(result).not_to have_header + expect(result.content).to eq({ a: 1 }) + end + + context 'with explicit document start marker' do + let(:yaml) do + <<~YAML + --- + a: 1 + --- + b: 2 + YAML + end + + it 'considers the first document as content for backwards compatibility' do + expect(result).to be_valid + expect(result.error).to be_nil + expect(result).not_to have_header + expect(result.content).to eq({ a: 1 }) + end + end + end + + context 'when the first document is not a header and second document is empty' do + let(:yaml) do + <<~YAML + a: 1 + --- + YAML + end + + it 'considers the first document as content' do + expect(result).to be_valid + expect(result.error).to be_nil + expect(result).not_to have_header + expect(result.content).to eq({ a: 1 }) + end + + context 'with explicit document start marker' do + let(:yaml) do + <<~YAML + --- + a: 1 + --- + YAML + end + + it 'considers the first document as content' do + expect(result).to be_valid + expect(result.error).to be_nil + expect(result).not_to have_header + expect(result.content).to eq({ a: 1 }) + end + end + end + end end diff --git a/spec/lib/gitlab/ci/config_spec.rb b/spec/lib/gitlab/ci/config_spec.rb index 5cdc9c21561..fdf152b3584 100644 --- a/spec/lib/gitlab/ci/config_spec.rb +++ b/spec/lib/gitlab/ci/config_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::Config, feature_category: :pipeline_authoring do +RSpec.describe Gitlab::Ci::Config, feature_category: :pipeline_composition do include StubRequests include RepoHelpers diff --git a/spec/lib/gitlab/ci/input/arguments/base_spec.rb b/spec/lib/gitlab/ci/input/arguments/base_spec.rb new file mode 100644 index 00000000000..ed8e99b7257 --- /dev/null +++ b/spec/lib/gitlab/ci/input/arguments/base_spec.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' + +RSpec.describe Gitlab::Ci::Input::Arguments::Base, feature_category: :pipeline_composition do + subject do + Class.new(described_class) do + def validate!; end + def to_value; end + end + end + + it 'fabricates an invalid input argument if unknown value is provided' do + argument = subject.new(:something, { spec: 123 }, [:a, :b]) + + expect(argument).not_to be_valid + expect(argument.errors.first).to eq 'unsupported value in input argument `something`' + end +end diff --git a/spec/lib/gitlab/ci/input/arguments/default_spec.rb b/spec/lib/gitlab/ci/input/arguments/default_spec.rb new file mode 100644 index 00000000000..bc0cee6ac4e --- /dev/null +++ b/spec/lib/gitlab/ci/input/arguments/default_spec.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' + +RSpec.describe Gitlab::Ci::Input::Arguments::Default, feature_category: :pipeline_composition do + it 'returns a user-provided value if it is present' do + argument = described_class.new(:website, { default: 'https://gitlab.com' }, 'https://example.gitlab.com') + + expect(argument).to be_valid + expect(argument.to_value).to eq 'https://example.gitlab.com' + expect(argument.to_hash).to eq({ website: 'https://example.gitlab.com' }) + end + + it 'returns an empty value if user-provider input is empty' do + argument = described_class.new(:website, { default: 'https://gitlab.com' }, '') + + expect(argument).to be_valid + expect(argument.to_value).to eq '' + expect(argument.to_hash).to eq({ website: '' }) + end + + it 'returns a default value if user-provider one is unknown' do + argument = described_class.new(:website, { default: 'https://gitlab.com' }, nil) + + expect(argument).to be_valid + expect(argument.to_value).to eq 'https://gitlab.com' + expect(argument.to_hash).to eq({ website: 'https://gitlab.com' }) + end + + it 'returns an error if the default argument has not been recognized' do + argument = described_class.new(:website, { default: ['gitlab.com'] }, 'abc') + + expect(argument).not_to be_valid + end + + it 'returns an error if the argument has not been fabricated correctly' do + argument = described_class.new(:website, { required: 'https://gitlab.com' }, 'https://example.gitlab.com') + + expect(argument).not_to be_valid + end + + describe '.matches?' do + it 'matches specs with default configuration' do + expect(described_class.matches?({ default: 'abc' })).to be true + end + + it 'does not match specs different configuration keyword' do + expect(described_class.matches?({ options: %w[a b] })).to be false + expect(described_class.matches?('a b c')).to be false + expect(described_class.matches?(%w[default a])).to be false + end + end +end diff --git a/spec/lib/gitlab/ci/input/arguments/options_spec.rb b/spec/lib/gitlab/ci/input/arguments/options_spec.rb new file mode 100644 index 00000000000..17e3469b294 --- /dev/null +++ b/spec/lib/gitlab/ci/input/arguments/options_spec.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' + +RSpec.describe Gitlab::Ci::Input::Arguments::Options, feature_category: :pipeline_composition do + it 'returns a user-provided value if it is an allowed one' do + argument = described_class.new(:run, { options: %w[opt1 opt2] }, 'opt1') + + expect(argument).to be_valid + expect(argument.to_value).to eq 'opt1' + expect(argument.to_hash).to eq({ run: 'opt1' }) + end + + it 'returns an error if user-provided value is not allowlisted' do + argument = described_class.new(:run, { options: %w[opt1 opt2] }, 'opt3') + + expect(argument).not_to be_valid + expect(argument.errors.first).to eq '`run` input: argument value opt3 not allowlisted' + end + + it 'returns an error if specification is not correct' do + argument = described_class.new(:website, { options: nil }, 'opt1') + + expect(argument).not_to be_valid + expect(argument.errors.first).to eq '`website` input: argument specification invalid' + end + + it 'returns an error if specification is using a hash' do + argument = described_class.new(:website, { options: { a: 1 } }, 'opt1') + + expect(argument).not_to be_valid + expect(argument.errors.first).to eq '`website` input: argument specification invalid' + end + + it 'returns an empty value if it is allowlisted' do + argument = described_class.new(:run, { options: ['opt1', ''] }, '') + + expect(argument).to be_valid + expect(argument.to_value).to be_empty + expect(argument.to_hash).to eq({ run: '' }) + end + + describe '.matches?' do + it 'matches specs with options configuration' do + expect(described_class.matches?({ options: %w[a b] })).to be true + end + + it 'does not match specs different configuration keyword' do + expect(described_class.matches?({ default: 'abc' })).to be false + expect(described_class.matches?(['options'])).to be false + expect(described_class.matches?('options')).to be false + end + end +end diff --git a/spec/lib/gitlab/ci/input/arguments/required_spec.rb b/spec/lib/gitlab/ci/input/arguments/required_spec.rb new file mode 100644 index 00000000000..847272998c2 --- /dev/null +++ b/spec/lib/gitlab/ci/input/arguments/required_spec.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' + +RSpec.describe Gitlab::Ci::Input::Arguments::Required, feature_category: :pipeline_composition do + it 'returns a user-provided value if it is present' do + argument = described_class.new(:website, nil, 'https://example.gitlab.com') + + expect(argument).to be_valid + expect(argument.to_value).to eq 'https://example.gitlab.com' + expect(argument.to_hash).to eq({ website: 'https://example.gitlab.com' }) + end + + it 'returns an empty value if user-provider value is empty' do + argument = described_class.new(:website, nil, '') + + expect(argument).to be_valid + expect(argument.to_hash).to eq(website: '') + end + + it 'returns an error if user-provided value is unspecified' do + argument = described_class.new(:website, nil, nil) + + expect(argument).not_to be_valid + expect(argument.errors.first).to eq '`website` input: required value has not been provided' + end + + describe '.matches?' do + it 'matches specs without configuration' do + expect(described_class.matches?(nil)).to be true + end + + it 'matches specs with empty configuration' do + expect(described_class.matches?('')).to be true + end + + it 'matches specs with an empty hash configuration' do + expect(described_class.matches?({})).to be true + end + + it 'does not match specs with configuration' do + expect(described_class.matches?({ options: %w[a b] })).to be false + end + end +end diff --git a/spec/lib/gitlab/ci/input/arguments/unknown_spec.rb b/spec/lib/gitlab/ci/input/arguments/unknown_spec.rb new file mode 100644 index 00000000000..1270423ac72 --- /dev/null +++ b/spec/lib/gitlab/ci/input/arguments/unknown_spec.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' + +RSpec.describe Gitlab::Ci::Input::Arguments::Unknown, feature_category: :pipeline_composition do + it 'raises an error when someone tries to evaluate the value' do + argument = described_class.new(:website, nil, 'https://example.gitlab.com') + + expect(argument).not_to be_valid + expect { argument.to_value }.to raise_error ArgumentError + end + + describe '.matches?' do + it 'always matches' do + expect(described_class.matches?('abc')).to be true + end + end +end diff --git a/spec/lib/gitlab/ci/input/inputs_spec.rb b/spec/lib/gitlab/ci/input/inputs_spec.rb new file mode 100644 index 00000000000..5d2d5192299 --- /dev/null +++ b/spec/lib/gitlab/ci/input/inputs_spec.rb @@ -0,0 +1,126 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' + +RSpec.describe Gitlab::Ci::Input::Inputs, feature_category: :pipeline_composition do + describe '#valid?' do + let(:spec) { { website: nil } } + + it 'describes user-provided inputs' do + inputs = described_class.new(spec, { website: 'http://example.gitlab.com' }) + + expect(inputs).to be_valid + end + end + + context 'when proper specification has been provided' do + let(:spec) do + { + website: nil, + env: { default: 'development' }, + run: { options: %w[tests spec e2e] } + } + end + + let(:args) { { website: 'https://gitlab.com', run: 'tests' } } + + it 'fabricates desired input arguments' do + inputs = described_class.new(spec, args) + + expect(inputs).to be_valid + expect(inputs.count).to eq 3 + expect(inputs.to_hash).to eq(args.merge(env: 'development')) + end + end + + context 'when inputs and args are empty' do + it 'is a valid use-case' do + inputs = described_class.new({}, {}) + + expect(inputs).to be_valid + expect(inputs.to_hash).to be_empty + end + end + + context 'when there are arguments recoincilation errors present' do + context 'when required argument is missing' do + let(:spec) { { website: nil } } + + it 'returns an error' do + inputs = described_class.new(spec, {}) + + expect(inputs).not_to be_valid + expect(inputs.errors.first).to eq '`website` input: required value has not been provided' + end + end + + context 'when argument is not present but configured as allowlist' do + let(:spec) do + { run: { options: %w[opt1 opt2] } } + end + + it 'returns an error' do + inputs = described_class.new(spec, {}) + + expect(inputs).not_to be_valid + expect(inputs.errors.first).to eq '`run` input: argument not provided' + end + end + end + + context 'when unknown specification argument has been used' do + let(:spec) do + { + website: nil, + env: { default: 'development' }, + run: { options: %w[tests spec e2e] }, + test: { unknown: 'something' } + } + end + + let(:args) { { website: 'https://gitlab.com', run: 'tests' } } + + it 'fabricates an unknown argument entry and returns an error' do + inputs = described_class.new(spec, args) + + expect(inputs).not_to be_valid + expect(inputs.count).to eq 4 + expect(inputs.errors.first).to eq '`test` input: unrecognized input argument specification: `unknown`' + end + end + + context 'when unknown arguments are being passed by a user' do + let(:spec) do + { env: { default: 'development' } } + end + + let(:args) { { website: 'https://gitlab.com', run: 'tests' } } + + it 'returns an error with a list of unknown arguments' do + inputs = described_class.new(spec, args) + + expect(inputs).not_to be_valid + expect(inputs.errors.first).to eq 'unknown input arguments: [:website, :run]' + end + end + + context 'when composite specification is being used' do + let(:spec) do + { + env: { + default: 'dev', + options: %w[test dev prod] + } + } + end + + let(:args) { { env: 'dev' } } + + it 'returns an error describing an unknown specification' do + inputs = described_class.new(spec, args) + + expect(inputs).not_to be_valid + expect(inputs.errors.first).to eq '`env` input: unrecognized input argument definition' + end + end +end diff --git a/spec/lib/gitlab/ci/interpolation/access_spec.rb b/spec/lib/gitlab/ci/interpolation/access_spec.rb index 9f6108a328d..f327377b7e3 100644 --- a/spec/lib/gitlab/ci/interpolation/access_spec.rb +++ b/spec/lib/gitlab/ci/interpolation/access_spec.rb @@ -2,7 +2,7 @@ require 'fast_spec_helper' -RSpec.describe Gitlab::Ci::Interpolation::Access, feature_category: :pipeline_authoring do +RSpec.describe Gitlab::Ci::Interpolation::Access, feature_category: :pipeline_composition do subject { described_class.new(access, ctx) } let(:access) do diff --git a/spec/lib/gitlab/ci/interpolation/block_spec.rb b/spec/lib/gitlab/ci/interpolation/block_spec.rb index 7f2be505d17..4a8709df3dc 100644 --- a/spec/lib/gitlab/ci/interpolation/block_spec.rb +++ b/spec/lib/gitlab/ci/interpolation/block_spec.rb @@ -2,7 +2,7 @@ require 'fast_spec_helper' -RSpec.describe Gitlab::Ci::Interpolation::Block, feature_category: :pipeline_authoring do +RSpec.describe Gitlab::Ci::Interpolation::Block, feature_category: :pipeline_composition do subject { described_class.new(block, data, ctx) } let(:data) do diff --git a/spec/lib/gitlab/ci/interpolation/config_spec.rb b/spec/lib/gitlab/ci/interpolation/config_spec.rb index e5987776e00..e745269d8c0 100644 --- a/spec/lib/gitlab/ci/interpolation/config_spec.rb +++ b/spec/lib/gitlab/ci/interpolation/config_spec.rb @@ -2,7 +2,7 @@ require 'fast_spec_helper' -RSpec.describe Gitlab::Ci::Interpolation::Config, feature_category: :pipeline_authoring do +RSpec.describe Gitlab::Ci::Interpolation::Config, feature_category: :pipeline_composition do subject { described_class.new(YAML.safe_load(config)) } let(:config) do diff --git a/spec/lib/gitlab/ci/interpolation/context_spec.rb b/spec/lib/gitlab/ci/interpolation/context_spec.rb index ada896f4980..2b126f4a8b3 100644 --- a/spec/lib/gitlab/ci/interpolation/context_spec.rb +++ b/spec/lib/gitlab/ci/interpolation/context_spec.rb @@ -2,7 +2,7 @@ require 'fast_spec_helper' -RSpec.describe Gitlab::Ci::Interpolation::Context, feature_category: :pipeline_authoring do +RSpec.describe Gitlab::Ci::Interpolation::Context, feature_category: :pipeline_composition do subject { described_class.new(ctx) } let(:ctx) do diff --git a/spec/lib/gitlab/ci/interpolation/template_spec.rb b/spec/lib/gitlab/ci/interpolation/template_spec.rb index 8a243b4db05..a3ef1bb4445 100644 --- a/spec/lib/gitlab/ci/interpolation/template_spec.rb +++ b/spec/lib/gitlab/ci/interpolation/template_spec.rb @@ -2,7 +2,7 @@ require 'fast_spec_helper' -RSpec.describe Gitlab::Ci::Interpolation::Template, feature_category: :pipeline_authoring do +RSpec.describe Gitlab::Ci::Interpolation::Template, feature_category: :pipeline_composition do subject { described_class.new(YAML.safe_load(config), ctx) } let(:config) do diff --git a/spec/lib/gitlab/ci/jwt_spec.rb b/spec/lib/gitlab/ci/jwt_spec.rb index 147801b6217..a6de5b9879c 100644 --- a/spec/lib/gitlab/ci/jwt_spec.rb +++ b/spec/lib/gitlab/ci/jwt_spec.rb @@ -58,26 +58,31 @@ RSpec.describe Gitlab::Ci::Jwt do expect { payload }.not_to raise_error end - describe 'ref type' do - context 'branches' do + describe 'references' do + context 'with a branch pipepline' do it 'is "branch"' do expect(payload[:ref_type]).to eq('branch') + expect(payload[:ref_path]).to eq('refs/heads/auto-deploy-2020-03-19') end end - context 'tags' do - let(:build) { build_stubbed(:ci_build, :on_tag, project: project) } + context 'with a tag pipeline' do + let(:pipeline) { build_stubbed(:ci_pipeline, ref: 'auto-deploy-2020-03-19', tag: true) } + let(:build) { build_stubbed(:ci_build, :on_tag, project: project, pipeline: pipeline) } it 'is "tag"' do expect(payload[:ref_type]).to eq('tag') + expect(payload[:ref_path]).to eq('refs/tags/auto-deploy-2020-03-19') end end - context 'merge requests' do - let(:pipeline) { build_stubbed(:ci_pipeline, :detached_merge_request_pipeline) } + context 'with a merge request pipeline' do + let(:merge_request) { build_stubbed(:merge_request, source_branch: 'feature-branch') } + let(:pipeline) { build_stubbed(:ci_pipeline, :detached_merge_request_pipeline, merge_request: merge_request) } it 'is "branch"' do expect(payload[:ref_type]).to eq('branch') + expect(payload[:ref_path]).to eq('refs/heads/feature-branch') end end end diff --git a/spec/lib/gitlab/ci/jwt_v2_spec.rb b/spec/lib/gitlab/ci/jwt_v2_spec.rb index 5eeab658a8e..528be4b5da7 100644 --- a/spec/lib/gitlab/ci/jwt_v2_spec.rb +++ b/spec/lib/gitlab/ci/jwt_v2_spec.rb @@ -2,11 +2,18 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::JwtV2 do +RSpec.describe Gitlab::Ci::JwtV2, feature_category: :continuous_integration do let(:namespace) { build_stubbed(:namespace) } let(:project) { build_stubbed(:project, namespace: namespace) } - let(:user) { build_stubbed(:user) } + let(:user) do + build_stubbed( + :user, + identities: [build_stubbed(:identity, extern_uid: '1', provider: 'github')] + ) + end + let(:pipeline) { build_stubbed(:ci_pipeline, ref: 'auto-deploy-2020-03-19') } + let(:runner) { build_stubbed(:ci_runner) } let(:aud) { described_class::DEFAULT_AUD } let(:build) do @@ -14,7 +21,8 @@ RSpec.describe Gitlab::Ci::JwtV2 do :ci_build, project: project, user: user, - pipeline: pipeline + pipeline: pipeline, + runner: runner ) end @@ -33,6 +41,18 @@ RSpec.describe Gitlab::Ci::JwtV2 do end end + it 'includes user identities when enabled' do + expect(user).to receive(:pass_user_identities_to_ci_jwt).and_return(true) + identities = payload[:user_identities].map { |identity| identity.slice(:extern_uid, :provider) } + expect(identities).to eq([{ extern_uid: '1', provider: 'github' }]) + end + + it 'does not include user identities when disabled' do + expect(user).to receive(:pass_user_identities_to_ci_jwt).and_return(false) + + expect(payload).not_to include(:user_identities) + end + context 'when given an aud' do let(:aud) { 'AWS' } @@ -40,5 +60,57 @@ RSpec.describe Gitlab::Ci::JwtV2 do expect(payload[:aud]).to eq('AWS') end end + + describe 'custom claims' do + describe 'runner_id' do + it 'is the ID of the runner executing the job' do + expect(payload[:runner_id]).to eq(runner.id) + end + + context 'when build is not associated with a runner' do + let(:runner) { nil } + + it 'is nil' do + expect(payload[:runner_id]).to be_nil + end + end + end + + describe 'runner_environment' do + context 'when runner is gitlab-hosted' do + before do + allow(runner).to receive(:gitlab_hosted?).and_return(true) + end + + it "is #{described_class::GITLAB_HOSTED_RUNNER}" do + expect(payload[:runner_environment]).to eq(described_class::GITLAB_HOSTED_RUNNER) + end + end + + context 'when runner is self-hosted' do + before do + allow(runner).to receive(:gitlab_hosted?).and_return(false) + end + + it "is #{described_class::SELF_HOSTED_RUNNER}" do + expect(payload[:runner_environment]).to eq(described_class::SELF_HOSTED_RUNNER) + end + end + + context 'when build is not associated with a runner' do + let(:runner) { nil } + + it 'is nil' do + expect(payload[:runner_environment]).to be_nil + end + end + end + + describe 'sha' do + it 'is the commit revision the project is built for' do + expect(payload[:sha]).to eq(pipeline.sha) + end + end + end end end diff --git a/spec/lib/gitlab/ci/lint_spec.rb b/spec/lib/gitlab/ci/lint_spec.rb index b836ca395fa..b238e9161eb 100644 --- a/spec/lib/gitlab/ci/lint_spec.rb +++ b/spec/lib/gitlab/ci/lint_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::Lint, feature_category: :pipeline_authoring do +RSpec.describe Gitlab::Ci::Lint, feature_category: :pipeline_composition do let_it_be(:project) { create(:project, :repository) } let_it_be(:user) { create(:user) } @@ -100,8 +100,8 @@ RSpec.describe Gitlab::Ci::Lint, feature_category: :pipeline_authoring do end it 'sets merged_config' do - root_config = YAML.safe_load(content, [Symbol]) - included_config = YAML.safe_load(included_content, [Symbol]) + root_config = YAML.safe_load(content, permitted_classes: [Symbol]) + included_config = YAML.safe_load(included_content, permitted_classes: [Symbol]) expected_config = included_config.merge(root_config).except(:include).deep_stringify_keys expect(subject.merged_yaml).to eq(expected_config.to_yaml) diff --git a/spec/lib/gitlab/ci/parsers/security/common_spec.rb b/spec/lib/gitlab/ci/parsers/security/common_spec.rb index 5d2d22c04fc..421aa29f860 100644 --- a/spec/lib/gitlab/ci/parsers/security/common_spec.rb +++ b/spec/lib/gitlab/ci/parsers/security/common_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::Parsers::Security::Common do +RSpec.describe Gitlab::Ci::Parsers::Security::Common, feature_category: :vulnerability_management do describe '#parse!' do let_it_be(:scanner_data) do { @@ -410,6 +410,12 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Common do end end + describe 'setting the `found_by_pipeline` attribute' do + subject { report.findings.map(&:found_by_pipeline).uniq } + + it { is_expected.to eq([pipeline]) } + end + describe 'parsing tracking' do let(:finding) { report.findings.first } diff --git a/spec/lib/gitlab/ci/parsers/security/sast_spec.rb b/spec/lib/gitlab/ci/parsers/security/sast_spec.rb index f6113308201..d1ce6808d23 100644 --- a/spec/lib/gitlab/ci/parsers/security/sast_spec.rb +++ b/spec/lib/gitlab/ci/parsers/security/sast_spec.rb @@ -13,8 +13,8 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Sast do context "when passing valid report" do # rubocop: disable Layout/LineLength where(:report_format, :report_version, :scanner_length, :finding_length, :identifier_length, :file_path, :start_line, :end_line, :primary_identifiers_length) do - :sast | '14.0.0' | 1 | 5 | 6 | 'groovy/src/main/java/com/gitlab/security_products/tests/App.groovy' | 47 | 47 | nil - :sast_semgrep_for_multiple_findings | '14.0.4' | 1 | 2 | 6 | 'app/app.py' | 39 | nil | 2 + :sast | '15.0.0' | 1 | 5 | 6 | 'groovy/src/main/java/com/gitlab/security_products/tests/App.groovy' | 47 | 47 | nil + :sast_semgrep_for_multiple_findings | '15.0.4' | 1 | 2 | 6 | 'app/app.py' | 39 | nil | 2 end # rubocop: enable Layout/LineLength diff --git a/spec/lib/gitlab/ci/parsers/security/secret_detection_spec.rb b/spec/lib/gitlab/ci/parsers/security/secret_detection_spec.rb index e8f1d617cb7..13999b2a9e5 100644 --- a/spec/lib/gitlab/ci/parsers/security/secret_detection_spec.rb +++ b/spec/lib/gitlab/ci/parsers/security/secret_detection_spec.rb @@ -39,7 +39,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::SecretDetection do end it "generates expected metadata_version" do - expect(report.findings.first.metadata_version).to eq('14.1.2') + expect(report.findings.first.metadata_version).to eq('15.0.0') end end end diff --git a/spec/lib/gitlab/ci/parsers/security/validators/schema_validator_spec.rb b/spec/lib/gitlab/ci/parsers/security/validators/schema_validator_spec.rb index 5fbaae58a73..2064a592246 100644 --- a/spec/lib/gitlab/ci/parsers/security/validators/schema_validator_spec.rb +++ b/spec/lib/gitlab/ci/parsers/security/validators/schema_validator_spec.rb @@ -5,55 +5,42 @@ require 'spec_helper' RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator, feature_category: :vulnerability_management do let_it_be(:project) { create(:project) } - let(:current_dast_versions) { described_class::CURRENT_VERSIONS[:dast].join(', ') } let(:supported_dast_versions) { described_class::SUPPORTED_VERSIONS[:dast].join(', ') } - let(:deprecated_schema_version_message) {} - let(:missing_schema_version_message) do - "Report version not provided, dast report type supports versions: #{supported_dast_versions}" - end let(:scanner) do { - 'id' => 'gemnasium', - 'name' => 'Gemnasium', - 'version' => '2.1.0' + 'id' => 'my-dast-scanner', + 'name' => 'My DAST scanner', + 'version' => '0.2.0', + 'vendor' => { 'name' => 'A DAST scanner' } } end - let(:analyzer_vendor) do - { 'name' => 'A DAST analyzer' } - end - - let(:scanner_vendor) do - { 'name' => 'A DAST scanner' } - end + let(:report_type) { :dast } - let(:report_data) do + let(:valid_data) do { 'scan' => { 'analyzer' => { 'id' => 'my-dast-analyzer', 'name' => 'My DAST analyzer', 'version' => '0.1.0', - 'vendor' => analyzer_vendor + 'vendor' => { 'name' => 'A DAST analyzer' } }, 'end_time' => '2020-01-28T03:26:02', 'scanned_resources' => [], - 'scanner' => { - 'id' => 'my-dast-scanner', - 'name' => 'My DAST scanner', - 'version' => '0.2.0', - 'vendor' => scanner_vendor - }, + 'scanner' => scanner, 'start_time' => '2020-01-28T03:26:01', 'status' => 'success', - 'type' => 'dast' + 'type' => report_type.to_s }, 'version' => report_version, 'vulnerabilities' => [] } end + let(:report_data) { valid_data } + let(:validator) { described_class.new(report_type, report_data, report_version, project: project, scanner: scanner) } shared_examples 'report is valid' do @@ -70,8 +57,8 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator, featu security_report_version: report_version, project_id: project.id, security_report_failure: security_report_failure, - security_report_scanner_id: 'gemnasium', - security_report_scanner_version: '2.1.0' + security_report_scanner_id: scanner['id'], + security_report_scanner_version: scanner['version'] ) subject @@ -142,7 +129,6 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator, featu subject { validator.valid? } context 'when given a supported MAJOR.MINOR schema version' do - let(:report_type) { :dast } let(:report_version) do latest_vendored_version = described_class::SUPPORTED_VERSIONS[report_type].last.split(".") (latest_vendored_version[0...2] << "34").join(".") @@ -153,7 +139,6 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator, featu end context 'when given a supported schema version' do - let(:report_type) { :dast } let(:report_version) { described_class::SUPPORTED_VERSIONS[report_type].last } it_behaves_like 'report is valid' @@ -161,7 +146,6 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator, featu end context 'when given a deprecated schema version' do - let(:report_type) { :dast } let(:deprecations_hash) do { dast: %w[10.0.0] @@ -175,13 +159,6 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator, featu end context 'and the report passes schema validation' do - let(:report_data) do - { - 'version' => '10.0.0', - 'vulnerabilities' => [] - } - end - let(:security_report_failure) { 'using_deprecated_schema_version' } it { is_expected.to be_truthy } @@ -191,9 +168,8 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator, featu context 'and the report does not pass schema validation' do let(:report_data) do - { - 'version' => 'V2.7.0' - } + valid_data.delete('vulnerabilities') + valid_data end it { is_expected.to be_falsey } @@ -201,17 +177,9 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator, featu end context 'when given an unsupported schema version' do - let(:report_type) { :dast } let(:report_version) { "12.37.0" } context 'and the report is valid' do - let(:report_data) do - { - 'version' => report_version, - 'vulnerabilities' => [] - } - end - let(:security_report_failure) { 'using_unsupported_schema_version' } it { is_expected.to be_falsey } @@ -259,8 +227,8 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator, featu end context 'when not given a schema version' do - let(:report_type) { :dast } let(:report_version) { nil } + let(:report_data) do { 'vulnerabilities' => [] @@ -285,21 +253,19 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator, featu subject { validator.errors } context 'when given a supported schema version' do - let(:report_type) { :dast } let(:report_version) { described_class::SUPPORTED_VERSIONS[report_type].last } it_behaves_like 'report is valid with no error' context 'and the report is invalid' do let(:report_data) do - { - 'version' => report_version - } + valid_data.delete('vulnerabilities') + valid_data end let(:expected_errors) do [ - 'root is missing required keys: scan, vulnerabilities' + 'root is missing required keys: vulnerabilities' ] end @@ -308,7 +274,6 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator, featu end context 'when given a deprecated schema version' do - let(:report_type) { :dast } let(:deprecations_hash) do { dast: %w[10.0.0] @@ -325,9 +290,9 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator, featu context 'and the report does not pass schema validation' do let(:report_data) do - { - 'version' => 'V2.7.0' - } + valid_data['version'] = "V2.7.0" + valid_data.delete('vulnerabilities') + valid_data end let(:expected_errors) do @@ -342,7 +307,6 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator, featu end context 'when given an unsupported schema version' do - let(:report_type) { :dast } let(:report_version) { "12.37.0" } let(:expected_unsupported_message) do "Version #{report_version} for report type #{report_type} is unsupported, supported versions for this report type are: "\ @@ -351,13 +315,6 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator, featu end context 'and the report is valid' do - let(:report_data) do - { - 'version' => report_version, - 'vulnerabilities' => [] - } - end - let(:expected_errors) do [ expected_unsupported_message @@ -369,9 +326,8 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator, featu context 'and the report is invalid' do let(:report_data) do - { - 'version' => report_version - } + valid_data.delete('vulnerabilities') + valid_data end let(:expected_errors) do @@ -386,7 +342,6 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator, featu end context 'when not given a schema version' do - let(:report_type) { :dast } let(:report_version) { nil } let(:expected_missing_version_message) do "Report version not provided, #{report_type} report type supports versions: #{supported_dast_versions}. GitLab "\ @@ -395,9 +350,8 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator, featu end let(:report_data) do - { - 'vulnerabilities' => [] - } + valid_data.delete('version') + valid_data end let(:expected_errors) do @@ -413,13 +367,6 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator, featu shared_examples 'report is valid with no warning' do context 'and the report is valid' do - let(:report_data) do - { - 'version' => report_version, - 'vulnerabilities' => [] - } - end - it { is_expected.to be_empty } end end @@ -432,25 +379,16 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator, featu subject { validator.deprecation_warnings } context 'when given a supported schema version' do - let(:report_type) { :dast } let(:report_version) { described_class::SUPPORTED_VERSIONS[report_type].last } context 'and the report is valid' do - let(:report_data) do - { - 'version' => report_version, - 'vulnerabilities' => [] - } - end - it { is_expected.to be_empty } end context 'and the report is invalid' do let(:report_data) do - { - 'version' => report_version - } + valid_data.delete('vulnerabilities') + valid_data end it { is_expected.to be_empty } @@ -458,7 +396,6 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator, featu end context 'when given a deprecated schema version' do - let(:report_type) { :dast } let(:deprecations_hash) do { dast: %w[V2.7.0] @@ -466,6 +403,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator, featu end let(:report_version) { described_class::DEPRECATED_VERSIONS[report_type].last } + let(:current_dast_versions) { described_class::CURRENT_VERSIONS[:dast].join(', ') } let(:expected_deprecation_message) do "version #{report_version} for report type #{report_type} is deprecated. "\ "However, GitLab will still attempt to parse and ingest this report. "\ @@ -483,53 +421,23 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator, featu end context 'and the report passes schema validation' do - let(:report_data) do - { - 'version' => report_version, - 'vulnerabilities' => [] - } - end - it_behaves_like 'report with expected warnings' end context 'and the report does not pass schema validation' do let(:report_data) do - { - 'version' => 'V2.7.0' - } + valid_data['version'] = "V2.7.0" + valid_data.delete('vulnerabilities') + valid_data end it_behaves_like 'report with expected warnings' end - - context 'and the report passes schema validation as a GitLab-vendored analyzer' do - let(:analyzer_vendor) do - { 'name' => 'GitLab' } - end - - it { is_expected.to be_empty } - end - - context 'and the report passes schema validation as a GitLab-vendored scanner' do - let(:scanner_vendor) do - { 'name' => 'GitLab' } - end - - it { is_expected.to be_empty } - end end context 'when given an unsupported schema version' do - let(:report_type) { :dast } let(:report_version) { "21.37.0" } let(:expected_deprecation_warnings) { [] } - let(:report_data) do - { - 'version' => report_version, - 'vulnerabilities' => [] - } - end it_behaves_like 'report with expected warnings' end @@ -539,7 +447,6 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator, featu subject { validator.warnings } context 'when given a supported MAJOR.MINOR schema version' do - let(:report_type) { :dast } let(:report_version) do latest_vendored_version = described_class::SUPPORTED_VERSIONS[report_type].last.split(".") (latest_vendored_version[0...2] << "34").join(".") @@ -559,13 +466,6 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator, featu end context 'and the report is valid' do - let(:report_data) do - { - 'version' => report_version, - 'vulnerabilities' => [] - } - end - it { is_expected.to match_array([message]) } context 'without license', unless: Gitlab.ee? do @@ -607,7 +507,6 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator, featu end context 'when given a supported schema version' do - let(:report_type) { :dast } let(:report_version) { described_class::SUPPORTED_VERSIONS[report_type].last } it_behaves_like 'report is valid with no warning' @@ -624,34 +523,26 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator, featu end context 'when given a deprecated schema version' do - let(:report_type) { :dast } + let(:deprecated_version) { '14.1.3' } + let(:report_version) { deprecated_version } let(:deprecations_hash) do { - dast: %w[V2.7.0] + dast: %w[deprecated_version] } end - let(:report_version) { described_class::DEPRECATED_VERSIONS[report_type].last } - before do stub_const("#{described_class}::DEPRECATED_VERSIONS", deprecations_hash) end context 'and the report passes schema validation' do - let(:report_data) do - { - 'vulnerabilities' => [] - } - end - it { is_expected.to be_empty } end context 'and the report does not pass schema validation' do let(:report_data) do - { - 'version' => 'V2.7.0' - } + valid_data.delete('vulnerabilities') + valid_data end it { is_expected.to be_empty } @@ -659,7 +550,6 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator, featu end context 'when given an unsupported schema version' do - let(:report_type) { :dast } let(:report_version) { "12.37.0" } it_behaves_like 'report is valid with no warning' @@ -676,13 +566,7 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator, featu end context 'when not given a schema version' do - let(:report_type) { :dast } let(:report_version) { nil } - let(:report_data) do - { - 'vulnerabilities' => [] - } - end it { is_expected.to be_empty } end diff --git a/spec/lib/gitlab/ci/pipeline/chain/config/content_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/config/content_spec.rb index e0d656f456e..a9a52972294 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/config/content_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/config/content_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::Pipeline::Chain::Config::Content do +RSpec.describe Gitlab::Ci::Pipeline::Chain::Config::Content, feature_category: :continuous_integration do let(:project) { create(:project, ci_config_path: ci_config_path) } let(:pipeline) { build(:ci_pipeline, project: project) } let(:content) { nil } @@ -26,6 +26,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Config::Content do expect(pipeline.config_source).to eq 'bridge_source' expect(command.config_content).to eq 'the-yaml' + expect(command.pipeline_config.internal_include_prepended?).to eq(false) end end @@ -52,6 +53,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Config::Content do expect(pipeline.config_source).to eq 'repository_source' expect(pipeline.pipeline_config.content).to eq(config_content_result) expect(command.config_content).to eq(config_content_result) + expect(command.pipeline_config.internal_include_prepended?).to eq(true) end end @@ -71,6 +73,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Config::Content do expect(pipeline.config_source).to eq 'remote_source' expect(pipeline.pipeline_config.content).to eq(config_content_result) expect(command.config_content).to eq(config_content_result) + expect(command.pipeline_config.internal_include_prepended?).to eq(true) end end @@ -91,6 +94,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Config::Content do expect(pipeline.config_source).to eq 'external_project_source' expect(pipeline.pipeline_config.content).to eq(config_content_result) expect(command.config_content).to eq(config_content_result) + expect(command.pipeline_config.internal_include_prepended?).to eq(true) end context 'when path specifies a refname' do @@ -111,6 +115,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Config::Content do expect(pipeline.config_source).to eq 'external_project_source' expect(pipeline.pipeline_config.content).to eq(config_content_result) expect(command.config_content).to eq(config_content_result) + expect(command.pipeline_config.internal_include_prepended?).to eq(true) end end end @@ -138,6 +143,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Config::Content do expect(pipeline.config_source).to eq 'repository_source' expect(pipeline.pipeline_config.content).to eq(config_content_result) expect(command.config_content).to eq(config_content_result) + expect(command.pipeline_config.internal_include_prepended?).to eq(true) end end @@ -161,6 +167,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Config::Content do expect(pipeline.config_source).to eq 'auto_devops_source' expect(pipeline.pipeline_config.content).to eq(config_content_result) expect(command.config_content).to eq(config_content_result) + expect(command.pipeline_config.internal_include_prepended?).to eq(true) end end @@ -181,6 +188,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Config::Content do expect(pipeline.config_source).to eq 'parameter_source' expect(pipeline.pipeline_config.content).to eq(content) expect(command.config_content).to eq(content) + expect(command.pipeline_config.internal_include_prepended?).to eq(false) end end @@ -197,6 +205,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Config::Content do expect(pipeline.config_source).to eq('unknown_source') expect(pipeline.pipeline_config).to be_nil expect(command.config_content).to be_nil + expect(command.pipeline_config).to be_nil expect(pipeline.errors.full_messages).to include('Missing CI config file') end end diff --git a/spec/lib/gitlab/ci/pipeline/duration_spec.rb b/spec/lib/gitlab/ci/pipeline/duration_spec.rb index 36714413da6..89c0ce46237 100644 --- a/spec/lib/gitlab/ci/pipeline/duration_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/duration_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::Pipeline::Duration do +RSpec.describe Gitlab::Ci::Pipeline::Duration, feature_category: :continuous_integration do describe '.from_periods' do let(:calculated_duration) { calculate(data) } @@ -113,16 +113,17 @@ RSpec.describe Gitlab::Ci::Pipeline::Duration do described_class::Period.new(first, last) end - described_class.from_periods(periods.sort_by(&:first)) + described_class.send(:from_periods, periods.sort_by(&:first)) end end describe '.from_pipeline' do + let_it_be_with_reload(:pipeline) { create(:ci_pipeline) } + let_it_be(:start_time) { Time.current.change(usec: 0) } let_it_be(:current) { start_time + 1000 } - let_it_be(:pipeline) { create(:ci_pipeline) } - let_it_be(:success_build) { create_build(:success, started_at: start_time, finished_at: start_time + 60) } - let_it_be(:failed_build) { create_build(:failed, started_at: start_time + 60, finished_at: start_time + 120) } + let_it_be(:success_build) { create_build(:success, started_at: start_time, finished_at: start_time + 50) } + let_it_be(:failed_build) { create_build(:failed, started_at: start_time + 60, finished_at: start_time + 110) } let_it_be(:canceled_build) { create_build(:canceled, started_at: start_time + 120, finished_at: start_time + 180) } let_it_be(:skipped_build) { create_build(:skipped, started_at: start_time) } let_it_be(:pending_build) { create_build(:pending) } @@ -141,21 +142,55 @@ RSpec.describe Gitlab::Ci::Pipeline::Duration do end context 'when there is no running build' do - let(:running_build) { nil } + let!(:running_build) { nil } it 'returns the duration for all the builds' do travel_to(current) do - expect(described_class.from_pipeline(pipeline)).to eq 180.seconds + # 160 = success (50) + failed (50) + canceled (60) + expect(described_class.from_pipeline(pipeline)).to eq 160.seconds end end end - context 'when there are bridge jobs' do - let!(:success_bridge) { create_bridge(:success, started_at: start_time + 220, finished_at: start_time + 280) } - let!(:failed_bridge) { create_bridge(:failed, started_at: start_time + 180, finished_at: start_time + 240) } - let!(:skipped_bridge) { create_bridge(:skipped, started_at: start_time) } - let!(:created_bridge) { create_bridge(:created) } - let!(:manual_bridge) { create_bridge(:manual) } + context 'when there are direct bridge jobs' do + let_it_be(:success_bridge) do + create_bridge(:success, started_at: start_time + 220, finished_at: start_time + 280) + end + + let_it_be(:failed_bridge) { create_bridge(:failed, started_at: start_time + 180, finished_at: start_time + 240) } + # NOTE: bridge won't be `canceled` as it will be marked as failed when downstream pipeline is canceled + # @see Ci::Bridge#inherit_status_from_downstream + let_it_be(:canceled_bridge) do + create_bridge(:failed, started_at: start_time + 180, finished_at: start_time + 210) + end + + let_it_be(:skipped_bridge) { create_bridge(:skipped, started_at: start_time) } + let_it_be(:created_bridge) { create_bridge(:created) } + let_it_be(:manual_bridge) { create_bridge(:manual) } + + let_it_be(:success_bridge_pipeline) do + create(:ci_pipeline, :success, started_at: start_time + 230, finished_at: start_time + 280).tap do |p| + create(:ci_sources_pipeline, source_job: success_bridge, pipeline: p) + create_build(:success, pipeline: p, started_at: start_time + 235, finished_at: start_time + 280) + create_bridge(:success, pipeline: p, started_at: start_time + 240, finished_at: start_time + 280) + end + end + + let_it_be(:failed_bridge_pipeline) do + create(:ci_pipeline, :failed, started_at: start_time + 225, finished_at: start_time + 240).tap do |p| + create(:ci_sources_pipeline, source_job: failed_bridge, pipeline: p) + create_build(:failed, pipeline: p, started_at: start_time + 230, finished_at: start_time + 240) + create_bridge(:success, pipeline: p, started_at: start_time + 235, finished_at: start_time + 240) + end + end + + let_it_be(:canceled_bridge_pipeline) do + create(:ci_pipeline, :canceled, started_at: start_time + 190, finished_at: start_time + 210).tap do |p| + create(:ci_sources_pipeline, source_job: canceled_bridge, pipeline: p) + create_build(:canceled, pipeline: p, started_at: start_time + 200, finished_at: start_time + 210) + create_bridge(:success, pipeline: p, started_at: start_time + 205, finished_at: start_time + 210) + end + end it 'returns the duration of the running build' do travel_to(current) do @@ -166,12 +201,99 @@ RSpec.describe Gitlab::Ci::Pipeline::Duration do context 'when there is no running build' do let!(:running_build) { nil } - it 'returns the duration for all the builds and bridge jobs' do + it 'returns the duration for all the builds (including self and downstreams)' do travel_to(current) do - expect(described_class.from_pipeline(pipeline)).to eq 280.seconds + # 220 = 160 (see above) + # + success build (45) + failed (10) + canceled (10) - overlapping (success & failed) (5) + expect(described_class.from_pipeline(pipeline)).to eq 220.seconds end end end + + # rubocop:disable RSpec/MultipleMemoizedHelpers + context 'when there are downstream bridge jobs' do + let_it_be(:success_direct_bridge) do + create_bridge(:success, started_at: start_time + 280, finished_at: start_time + 400) + end + + let_it_be(:success_downstream_pipeline) do + create(:ci_pipeline, :success, started_at: start_time + 285, finished_at: start_time + 300).tap do |p| + create(:ci_sources_pipeline, source_job: success_direct_bridge, pipeline: p) + create_build(:success, pipeline: p, started_at: start_time + 290, finished_at: start_time + 296) + create_bridge(:success, pipeline: p, started_at: start_time + 285, finished_at: start_time + 288) + end + end + + let_it_be(:failed_downstream_pipeline) do + create(:ci_pipeline, :failed, started_at: start_time + 305, finished_at: start_time + 350).tap do |p| + create(:ci_sources_pipeline, source_job: success_direct_bridge, pipeline: p) + create_build(:failed, pipeline: p, started_at: start_time + 320, finished_at: start_time + 327) + create_bridge(:success, pipeline: p, started_at: start_time + 305, finished_at: start_time + 350) + end + end + + let_it_be(:canceled_downstream_pipeline) do + create(:ci_pipeline, :canceled, started_at: start_time + 360, finished_at: start_time + 400).tap do |p| + create(:ci_sources_pipeline, source_job: success_direct_bridge, pipeline: p) + create_build(:canceled, pipeline: p, started_at: start_time + 390, finished_at: start_time + 398) + create_bridge(:success, pipeline: p, started_at: start_time + 360, finished_at: start_time + 378) + end + end + + it 'returns the duration of the running build' do + travel_to(current) do + expect(described_class.from_pipeline(pipeline)).to eq 1000.seconds + end + end + + context 'when there is no running build' do + let!(:running_build) { nil } + + it 'returns the duration for all the builds (including self and downstreams)' do + travel_to(current) do + # 241 = 220 (see above) + # + success downstream build (6) + failed (7) + canceled (8) + expect(described_class.from_pipeline(pipeline)).to eq 241.seconds + end + end + end + end + # rubocop:enable RSpec/MultipleMemoizedHelpers + end + + it 'does not generate N+1 queries if more builds are added' do + travel_to(current) do + expect do + described_class.from_pipeline(pipeline) + end.not_to exceed_query_limit(1) + + create_list(:ci_build, 2, :success, pipeline: pipeline, started_at: start_time, finished_at: start_time + 50) + + expect do + described_class.from_pipeline(pipeline) + end.not_to exceed_query_limit(1) + end + end + + it 'does not generate N+1 queries if more bridges and their pipeline builds are added' do + travel_to(current) do + expect do + described_class.from_pipeline(pipeline) + end.not_to exceed_query_limit(1) + + create_list( + :ci_bridge, 2, :success, + pipeline: pipeline, started_at: start_time + 220, finished_at: start_time + 280).each do |bridge| + create(:ci_pipeline, :success, started_at: start_time + 235, finished_at: start_time + 280).tap do |p| + create(:ci_sources_pipeline, source_job: bridge, pipeline: p) + create_builds(3, :success) + end + end + + expect do + described_class.from_pipeline(pipeline) + end.not_to exceed_query_limit(1) + end end private @@ -180,6 +302,10 @@ RSpec.describe Gitlab::Ci::Pipeline::Duration do create(:ci_build, trait, pipeline: pipeline, **opts) end + def create_builds(counts, trait, **opts) + create_list(:ci_build, counts, trait, pipeline: pipeline, **opts) + end + def create_bridge(trait, **opts) create(:ci_bridge, trait, pipeline: pipeline, **opts) end diff --git a/spec/lib/gitlab/ci/pipeline/seed/build/cache_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/build/cache_spec.rb index c264ea3bece..07e2d6960bf 100644 --- a/spec/lib/gitlab/ci/pipeline/seed/build/cache_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/seed/build/cache_spec.rb @@ -7,8 +7,9 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build::Cache do let_it_be(:head_sha) { project.repository.head_commit.id } let_it_be(:pipeline) { create(:ci_pipeline, project: project, sha: head_sha) } let(:index) { 1 } + let(:cache_prefix) { index } - let(:processor) { described_class.new(pipeline, config, index) } + let(:processor) { described_class.new(pipeline, config, cache_prefix) } describe '#attributes' do subject { processor.attributes } @@ -32,7 +33,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build::Cache do } end - it { is_expected.to include(config.merge(key: "a_key")) } + it { is_expected.to include(config.merge(key: 'a_key')) } end context 'with cache:key:files' do @@ -42,8 +43,8 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build::Cache do end context 'without a prefix' do - it 'uses default key with an index as a prefix' do - expected = { key: '1-default' } + it 'uses default key with an index and file names as a prefix' do + expected = { key: "#{cache_prefix}-default" } is_expected.to include(expected) end @@ -61,9 +62,9 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build::Cache do end context 'without a prefix' do - it 'builds a string key with an index as a prefix' do + it 'builds a string key with an index and file names as a prefix' do expected = { - key: '1-703ecc8fef1635427a1f86a8a1a308831c122392', + key: "#{cache_prefix}-703ecc8fef1635427a1f86a8a1a308831c122392", paths: ['vendor/ruby'] } @@ -74,30 +75,41 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build::Cache do context 'with existing files' do let(:files) { ['VERSION', 'Gemfile.zip'] } + let(:cache_prefix) { '1_VERSION_Gemfile' } it_behaves_like 'version and gemfile files' end context 'with files starting with ./' do let(:files) { ['Gemfile.zip', './VERSION'] } + let(:cache_prefix) { '1_Gemfile_' } it_behaves_like 'version and gemfile files' end + context 'with no files' do + let(:files) { [] } + + it_behaves_like 'default key' + end + context 'with files ending with /' do let(:files) { ['Gemfile.zip/'] } + let(:cache_prefix) { '1_Gemfile' } it_behaves_like 'default key' end context 'with new line in filenames' do - let(:files) { ["Gemfile.zip\nVERSION"] } + let(:files) { ['Gemfile.zip\nVERSION'] } + let(:cache_prefix) { '1_Gemfile' } it_behaves_like 'default key' end context 'with missing files' do let(:files) { ['project-gemfile.lock', ''] } + let(:cache_prefix) { '1_project-gemfile_' } it_behaves_like 'default key' end @@ -113,8 +125,8 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build::Cache do end context 'without a prefix' do - it 'builds a string key with an index as a prefix' do - expected = { key: '1-74bf43fb1090f161bdd4e265802775dbda2f03d1' } + it 'builds a string key with an index and file names as a prefix' do + expected = { key: "#{cache_prefix}-74bf43fb1090f161bdd4e265802775dbda2f03d1" } is_expected.to include(expected) end @@ -123,18 +135,21 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build::Cache do context 'with directory' do let(:files) { ['foo/bar'] } + let(:cache_prefix) { '1_foo/bar' } it_behaves_like 'foo/bar directory key' end context 'with directory ending in slash' do let(:files) { ['foo/bar/'] } + let(:cache_prefix) { '1_foo/bar/' } it_behaves_like 'foo/bar directory key' end context 'with directories ending in slash star' do let(:files) { ['foo/bar/*'] } + let(:cache_prefix) { '1_foo/bar/*' } it_behaves_like 'foo/bar directory key' end @@ -205,6 +220,18 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build::Cache do end end + context 'with cache:fallback_keys' do + let(:config) do + { + key: 'ruby-branch-key', + paths: ['vendor/ruby'], + fallback_keys: ['ruby-default'] + } + end + + it { is_expected.to include(config) } + end + context 'with all cache option keys' do let(:config) do { @@ -213,7 +240,8 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build::Cache do untracked: true, policy: 'push', unprotect: true, - when: 'on_success' + when: 'on_success', + fallback_keys: ['default-ruby'] } end diff --git a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb index 3043d7f5381..9d5a9bc8058 100644 --- a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::Pipeline::Seed::Build, feature_category: :pipeline_authoring do +RSpec.describe Gitlab::Ci::Pipeline::Seed::Build, feature_category: :pipeline_composition do let_it_be_with_reload(:project) { create(:project, :repository) } let_it_be(:head_sha) { project.repository.head_commit.id } @@ -109,6 +109,104 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build, feature_category: :pipeline_au end end + context 'with job:rules:[needs:]' do + context 'with a single rule' do + let(:job_needs_attributes) { [{ name: 'rspec' }] } + + context 'when job has needs set' do + context 'when rule evaluates to true' do + let(:attributes) do + { name: 'rspec', + ref: 'master', + needs_attributes: job_needs_attributes, + rules: [{ if: '$VAR == null', needs: { job: [{ name: 'build-job' }] } }] } + end + + it 'overrides the job needs' do + expect(subject).to include(needs_attributes: [{ name: 'build-job' }]) + end + end + + context 'when rule evaluates to false' do + let(:attributes) do + { name: 'rspec', + ref: 'master', + needs_attributes: job_needs_attributes, + rules: [{ if: '$VAR == true', needs: { job: [{ name: 'build-job' }] } }] } + end + + it 'keeps the job needs' do + expect(subject).to include(needs_attributes: job_needs_attributes) + end + end + + context 'with subkeys: artifacts, optional' do + let(:attributes) do + { name: 'rspec', + ref: 'master', + rules: + [ + { if: '$VAR == null', + needs: { + job: [{ + name: 'build-job', + optional: false, + artifacts: true + }] + } } + ] } + end + + context 'when rule evaluates to true' do + it 'sets the job needs as well as the job subkeys' do + expect(subject[:needs_attributes]).to match_array([{ name: 'build-job', optional: false, artifacts: true }]) + end + + it 'sets the scheduling type to dag' do + expect(subject[:scheduling_type]).to eq(:dag) + end + end + end + end + + context 'with multiple rules' do + context 'when a rule evaluates to true' do + let(:attributes) do + { name: 'rspec', + ref: 'master', + needs_attributes: job_needs_attributes, + rules: [ + { if: '$VAR == true', needs: { job: [{ name: 'rspec-1' }] } }, + { if: '$VAR2 == true', needs: { job: [{ name: 'rspec-2' }] } }, + { if: '$VAR3 == null', needs: { job: [{ name: 'rspec' }, { name: 'lint' }] } } + ] } + end + + it 'overrides the job needs' do + expect(subject).to include(needs_attributes: [{ name: 'rspec' }, { name: 'lint' }]) + end + end + + context 'when all rules evaluates to false' do + let(:attributes) do + { name: 'rspec', + ref: 'master', + needs_attributes: job_needs_attributes, + rules: [ + { if: '$VAR == true', needs: { job: [{ name: 'rspec-1' }] } }, + { if: '$VAR2 == true', needs: { job: [{ name: 'rspec-2' }] } }, + { if: '$VAR3 == true', needs: { job: [{ name: 'rspec-3' }] } } + ] } + end + + it 'keeps the job needs' do + expect(subject).to include(needs_attributes: job_needs_attributes) + end + end + end + end + end + context 'with job:tags' do let(:attributes) do { @@ -152,7 +250,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build, feature_category: :pipeline_au it 'includes cache options' do cache_options = { options: { - cache: [a_hash_including(key: '0-f155568ad0933d8358f66b846133614f76dd0ca4')] + cache: [a_hash_including(key: '0_VERSION-f155568ad0933d8358f66b846133614f76dd0ca4')] } } @@ -798,7 +896,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build, feature_category: :pipeline_au [ [[{ if: '$CI_JOB_NAME == "rspec" && $VAR == null', when: 'on_failure' }]], [[{ if: '$VARIABLE != null', when: 'delayed', start_in: '1 day' }, { if: '$CI_JOB_NAME == "rspec"', when: 'on_failure' }]], - [[{ if: '$VARIABLE == "the wrong value"', when: 'delayed', start_in: '1 day' }, { if: '$CI_BUILD_NAME == "rspec"', when: 'on_failure' }]] + [[{ if: '$VARIABLE == "the wrong value"', when: 'delayed', start_in: '1 day' }, { if: '$CI_JOB_NAME == "rspec"', when: 'on_failure' }]] ] end @@ -811,6 +909,30 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build, feature_category: :pipeline_au end end + context 'when FF `ci_remove_legacy_predefined_variables` is disabled' do + before do + stub_feature_flags(ci_remove_legacy_predefined_variables: false) + end + + context 'with an explicit `when: on_failure`' do + where(:rule_set) do + [ + [[{ if: '$CI_JOB_NAME == "rspec" && $VAR == null', when: 'on_failure' }]], + [[{ if: '$VARIABLE != null', when: 'delayed', start_in: '1 day' }, { if: '$CI_JOB_NAME == "rspec"', when: 'on_failure' }]], + [[{ if: '$VARIABLE == "the wrong value"', when: 'delayed', start_in: '1 day' }, { if: '$CI_BUILD_NAME == "rspec"', when: 'on_failure' }]] + ] + end + + with_them do + it { is_expected.to be_included } + + it 'correctly populates when:' do + expect(seed_build.attributes).to include(when: 'on_failure') + end + end + end + end + context 'with an explicit `when: delayed`' do where(:rule_set) do [ diff --git a/spec/lib/gitlab/ci/pipeline/seed/stage_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/stage_spec.rb index 288ac3f3854..ae40626510f 100644 --- a/spec/lib/gitlab/ci/pipeline/seed/stage_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/seed/stage_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::Pipeline::Seed::Stage, feature_category: :pipeline_authoring do +RSpec.describe Gitlab::Ci::Pipeline::Seed::Stage, feature_category: :pipeline_composition do let(:project) { create(:project, :repository) } let(:pipeline) { create(:ci_empty_pipeline, project: project) } let(:previous_stages) { [] } diff --git a/spec/lib/gitlab/ci/project_config/repository_spec.rb b/spec/lib/gitlab/ci/project_config/repository_spec.rb index 2105b691d9e..e8a997a7e43 100644 --- a/spec/lib/gitlab/ci/project_config/repository_spec.rb +++ b/spec/lib/gitlab/ci/project_config/repository_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::ProjectConfig::Repository do +RSpec.describe Gitlab::Ci::ProjectConfig::Repository, feature_category: :continuous_integration do let(:project) { create(:project, :custom_repo, files: files) } let(:sha) { project.repository.head_commit.sha } let(:files) { { 'README.md' => 'hello' } } @@ -44,4 +44,10 @@ RSpec.describe Gitlab::Ci::ProjectConfig::Repository do it { is_expected.to eq(:repository_source) } end + + describe '#internal_include_prepended?' do + subject { config.internal_include_prepended? } + + it { is_expected.to eq(true) } + end end diff --git a/spec/lib/gitlab/ci/project_config/source_spec.rb b/spec/lib/gitlab/ci/project_config/source_spec.rb index dda5c7cdce8..eefabe1babb 100644 --- a/spec/lib/gitlab/ci/project_config/source_spec.rb +++ b/spec/lib/gitlab/ci/project_config/source_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::ProjectConfig::Source do +RSpec.describe Gitlab::Ci::ProjectConfig::Source, feature_category: :continuous_integration do let_it_be(:custom_config_class) { Class.new(described_class) } let_it_be(:project) { build_stubbed(:project) } let_it_be(:sha) { '123456' } @@ -20,4 +20,10 @@ RSpec.describe Gitlab::Ci::ProjectConfig::Source do it { expect { source }.to raise_error(NotImplementedError) } end + + describe '#internal_include_prepended?' do + subject(:internal_include_prepended) { custom_config.internal_include_prepended? } + + it { expect(internal_include_prepended).to eq(false) } + end end diff --git a/spec/lib/gitlab/ci/reports/codequality_mr_diff_spec.rb b/spec/lib/gitlab/ci/reports/codequality_mr_diff_spec.rb index 73b916da2e9..79fa1c3ec75 100644 --- a/spec/lib/gitlab/ci/reports/codequality_mr_diff_spec.rb +++ b/spec/lib/gitlab/ci/reports/codequality_mr_diff_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::Reports::CodequalityMrDiff do +RSpec.describe Gitlab::Ci::Reports::CodequalityMrDiff, feature_category: :code_quality do let(:codequality_report) { Gitlab::Ci::Reports::CodequalityReports.new } let(:degradation_1) { build(:codequality_degradation_1) } let(:degradation_2) { build(:codequality_degradation_2) } diff --git a/spec/lib/gitlab/ci/reports/security/scanner_spec.rb b/spec/lib/gitlab/ci/reports/security/scanner_spec.rb index d7ac82e3b53..79c59fb0da8 100644 --- a/spec/lib/gitlab/ci/reports/security/scanner_spec.rb +++ b/spec/lib/gitlab/ci/reports/security/scanner_spec.rb @@ -131,7 +131,7 @@ RSpec.describe Gitlab::Ci::Reports::Security::Scanner do context 'when the `name` of the scanners are equal' do where(:scanner_1_attributes, :scanner_2_attributes, :expected_comparison_result) do - { external_id: 'gemnasium', name: 'foo', vendor: 'a' } | { external_id: 'gemnasium', name: 'foo', vendor: 'a' } | 0 # rubocop:disable Lint/BinaryOperatorWithIdenticalOperands + { external_id: 'gemnasium', name: 'foo', vendor: 'a' } | { external_id: 'gemnasium', name: 'foo', vendor: 'a' } | 0 { external_id: 'gemnasium', name: 'foo', vendor: 'a' } | { external_id: 'gemnasium', name: 'foo', vendor: 'b' } | -1 { external_id: 'gemnasium', name: 'foo', vendor: 'b' } | { external_id: 'gemnasium', name: 'foo', vendor: 'a' } | 1 end diff --git a/spec/lib/gitlab/ci/reports/security/vulnerability_reports_comparer_spec.rb b/spec/lib/gitlab/ci/reports/security/vulnerability_reports_comparer_spec.rb deleted file mode 100644 index 6f75e2c55e8..00000000000 --- a/spec/lib/gitlab/ci/reports/security/vulnerability_reports_comparer_spec.rb +++ /dev/null @@ -1,163 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::Ci::Reports::Security::VulnerabilityReportsComparer do - let(:identifier) { build(:ci_reports_security_identifier) } - - let_it_be(:project) { create(:project, :repository) } - - let(:location_param) { build(:ci_reports_security_locations_sast, :dynamic) } - let(:vulnerability_params) { vuln_params(project.id, [identifier], confidence: :low, severity: :critical) } - let(:base_vulnerability) { build(:ci_reports_security_finding, location: location_param, **vulnerability_params) } - let(:base_report) { build(:ci_reports_security_aggregated_reports, findings: [base_vulnerability]) } - - let(:head_vulnerability) { build(:ci_reports_security_finding, location: location_param, uuid: base_vulnerability.uuid, **vulnerability_params) } - let(:head_report) { build(:ci_reports_security_aggregated_reports, findings: [head_vulnerability]) } - - shared_context 'comparing reports' do - let(:vul_params) { vuln_params(project.id, [identifier]) } - let(:base_vulnerability) { build(:ci_reports_security_finding, :dynamic, **vul_params) } - let(:head_vulnerability) { build(:ci_reports_security_finding, :dynamic, **vul_params) } - let(:head_vul_findings) { [head_vulnerability, vuln] } - end - - subject { described_class.new(project, base_report, head_report) } - - where(vulnerability_finding_signatures: [true, false]) - - with_them do - before do - stub_licensed_features(vulnerability_finding_signatures: vulnerability_finding_signatures) - end - - describe '#base_report_out_of_date' do - context 'no base report' do - let(:base_report) { build(:ci_reports_security_aggregated_reports, reports: [], findings: []) } - - it 'is not out of date' do - expect(subject.base_report_out_of_date).to be false - end - end - - context 'base report older than one week' do - let(:report) { build(:ci_reports_security_report, created_at: 1.week.ago - 60.seconds) } - let(:base_report) { build(:ci_reports_security_aggregated_reports, reports: [report]) } - - it 'is not out of date' do - expect(subject.base_report_out_of_date).to be true - end - end - - context 'base report less than one week old' do - let(:report) { build(:ci_reports_security_report, created_at: 1.week.ago + 60.seconds) } - let(:base_report) { build(:ci_reports_security_aggregated_reports, reports: [report]) } - - it 'is not out of date' do - expect(subject.base_report_out_of_date).to be false - end - end - end - - describe '#added' do - let(:new_location) { build(:ci_reports_security_locations_sast, :dynamic) } - let(:vul_params) { vuln_params(project.id, [identifier], confidence: :high) } - let(:vuln) { build(:ci_reports_security_finding, severity: Enums::Vulnerability.severity_levels[:critical], location: new_location, **vul_params) } - let(:low_vuln) { build(:ci_reports_security_finding, severity: Enums::Vulnerability.severity_levels[:low], location: new_location, **vul_params) } - - context 'with new vulnerability' do - let(:head_report) { build(:ci_reports_security_aggregated_reports, findings: [head_vulnerability, vuln]) } - - it 'points to source tree' do - expect(subject.added).to eq([vuln]) - end - end - - context 'when comparing reports with different fingerprints' do - include_context 'comparing reports' - - let(:head_report) { build(:ci_reports_security_aggregated_reports, findings: head_vul_findings) } - - it 'does not find any overlap' do - expect(subject.added).to eq(head_vul_findings) - end - end - - context 'order' do - let(:head_report) { build(:ci_reports_security_aggregated_reports, findings: [head_vulnerability, vuln, low_vuln]) } - - it 'does not change' do - expect(subject.added).to eq([vuln, low_vuln]) - end - end - end - - describe '#fixed' do - let(:vul_params) { vuln_params(project.id, [identifier]) } - let(:vuln) { build(:ci_reports_security_finding, :dynamic, **vul_params ) } - let(:medium_vuln) { build(:ci_reports_security_finding, confidence: ::Enums::Vulnerability.confidence_levels[:high], severity: Enums::Vulnerability.severity_levels[:medium], uuid: vuln.uuid, **vul_params) } - - context 'with fixed vulnerability' do - let(:base_report) { build(:ci_reports_security_aggregated_reports, findings: [base_vulnerability, vuln]) } - - it 'points to base tree' do - expect(subject.fixed).to eq([vuln]) - end - end - - context 'when comparing reports with different fingerprints' do - include_context 'comparing reports' - - let(:base_report) { build(:ci_reports_security_aggregated_reports, findings: [base_vulnerability, vuln]) } - - it 'does not find any overlap' do - expect(subject.fixed).to eq([base_vulnerability, vuln]) - end - end - - context 'order' do - let(:vul_findings) { [vuln, medium_vuln] } - let(:base_report) { build(:ci_reports_security_aggregated_reports, findings: [*vul_findings, base_vulnerability]) } - - it 'does not change' do - expect(subject.fixed).to eq(vul_findings) - end - end - end - - describe 'with empty vulnerabilities' do - let(:empty_report) { build(:ci_reports_security_aggregated_reports, reports: [], findings: []) } - - it 'returns empty array when reports are not present' do - comparer = described_class.new(project, empty_report, empty_report) - - expect(comparer.fixed).to eq([]) - expect(comparer.added).to eq([]) - end - - it 'returns added vulnerability when base is empty and head is not empty' do - comparer = described_class.new(project, empty_report, head_report) - - expect(comparer.fixed).to eq([]) - expect(comparer.added).to eq([head_vulnerability]) - end - - it 'returns fixed vulnerability when head is empty and base is not empty' do - comparer = described_class.new(project, base_report, empty_report) - - expect(comparer.fixed).to eq([base_vulnerability]) - expect(comparer.added).to eq([]) - end - end - end - - def vuln_params(project_id, identifiers, confidence: :high, severity: :critical) - { - project_id: project_id, - report_type: :sast, - identifiers: identifiers, - confidence: ::Enums::Vulnerability.confidence_levels[confidence], - severity: ::Enums::Vulnerability.severity_levels[severity] - } - end -end diff --git a/spec/lib/gitlab/ci/runner_releases_spec.rb b/spec/lib/gitlab/ci/runner_releases_spec.rb index 14f3c95ec79..9e211327dee 100644 --- a/spec/lib/gitlab/ci/runner_releases_spec.rb +++ b/spec/lib/gitlab/ci/runner_releases_spec.rb @@ -177,6 +177,16 @@ RSpec.describe Gitlab::Ci::RunnerReleases, feature_category: :runner_fleet do it 'returns parsed and sorted Gitlab::VersionInfo objects' do expect(releases).to eq(expected_result) end + + context 'when fetching runner releases is disabled' do + before do + stub_application_setting(update_runner_versions_enabled: false) + end + + it 'returns nil' do + expect(releases).to be_nil + end + end end context 'when response contains unexpected input type' do @@ -218,6 +228,16 @@ RSpec.describe Gitlab::Ci::RunnerReleases, feature_category: :runner_fleet do it 'returns parsed and grouped Gitlab::VersionInfo objects' do expect(releases_by_minor).to eq(expected_result) end + + context 'when fetching runner releases is disabled' do + before do + stub_application_setting(update_runner_versions_enabled: false) + end + + it 'returns nil' do + expect(releases_by_minor).to be_nil + end + end end context 'when response contains unexpected input type' do @@ -233,6 +253,18 @@ RSpec.describe Gitlab::Ci::RunnerReleases, feature_category: :runner_fleet do end end + describe '#enabled?' do + it { is_expected.to be_enabled } + + context 'when fetching runner releases is disabled' do + before do + stub_application_setting(update_runner_versions_enabled: false) + end + + it { is_expected.not_to be_enabled } + end + end + def mock_http_response(response) http_response = instance_double(HTTParty::Response) diff --git a/spec/lib/gitlab/ci/secure_files/cer_spec.rb b/spec/lib/gitlab/ci/secure_files/cer_spec.rb index 6b9cd0e3bfc..76ce1785368 100644 --- a/spec/lib/gitlab/ci/secure_files/cer_spec.rb +++ b/spec/lib/gitlab/ci/secure_files/cer_spec.rb @@ -9,7 +9,7 @@ RSpec.describe Gitlab::Ci::SecureFiles::Cer do describe '#certificate_data' do it 'assigns the error message and returns nil' do expect(invalid_certificate.certificate_data).to be nil - expect(invalid_certificate.error).to eq('not enough data') + expect(invalid_certificate.error).to eq('PEM_read_bio_X509: no start line') end end @@ -50,7 +50,7 @@ RSpec.describe Gitlab::Ci::SecureFiles::Cer do describe '#expires_at' do it 'returns the certificate expiration timestamp' do - expect(subject.metadata[:expires_at]).to eq('2022-04-26 19:20:40 UTC') + expect(subject.metadata[:expires_at]).to eq('2023-04-26 19:20:39 UTC') end end diff --git a/spec/lib/gitlab/ci/secure_files/mobile_provision_spec.rb b/spec/lib/gitlab/ci/secure_files/mobile_provision_spec.rb index fb382174c64..1812b90df8b 100644 --- a/spec/lib/gitlab/ci/secure_files/mobile_provision_spec.rb +++ b/spec/lib/gitlab/ci/secure_files/mobile_provision_spec.rb @@ -10,7 +10,7 @@ RSpec.describe Gitlab::Ci::SecureFiles::MobileProvision do describe '#decoded_plist' do it 'assigns the error message and returns nil' do expect(invalid_profile.decoded_plist).to be nil - expect(invalid_profile.error).to eq('Could not parse the PKCS7: not enough data') + expect(invalid_profile.error).to eq('Could not parse the PKCS7: no start line') end end diff --git a/spec/lib/gitlab/ci/secure_files/p12_spec.rb b/spec/lib/gitlab/ci/secure_files/p12_spec.rb index beabf4b4856..7a855868ce8 100644 --- a/spec/lib/gitlab/ci/secure_files/p12_spec.rb +++ b/spec/lib/gitlab/ci/secure_files/p12_spec.rb @@ -62,7 +62,7 @@ RSpec.describe Gitlab::Ci::SecureFiles::P12 do describe '#expires_at' do it 'returns the certificate expiration timestamp' do - expect(subject.metadata[:expires_at]).to eq('2022-09-21 14:56:00 UTC') + expect(subject.metadata[:expires_at]).to eq('2023-09-21 14:55:59 UTC') end end diff --git a/spec/lib/gitlab/ci/status/composite_spec.rb b/spec/lib/gitlab/ci/status/composite_spec.rb index cceabc35e85..cbf0976c976 100644 --- a/spec/lib/gitlab/ci/status/composite_spec.rb +++ b/spec/lib/gitlab/ci/status/composite_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::Status::Composite do +RSpec.describe Gitlab::Ci::Status::Composite, feature_category: :continuous_integration do let_it_be(:pipeline) { create(:ci_pipeline) } before_all do @@ -15,6 +15,18 @@ RSpec.describe Gitlab::Ci::Status::Composite do end end + describe '.initialize' do + subject(:composite_status) { described_class.new(all_statuses) } + + context 'when passing a single status' do + let(:all_statuses) { @statuses[:success] } + + it 'raises ArgumentError' do + expect { composite_status }.to raise_error(ArgumentError, 'all_jobs needs to respond to `.pluck`') + end + end + end + describe '#status' do using RSpec::Parameterized::TableSyntax @@ -51,8 +63,8 @@ RSpec.describe Gitlab::Ci::Status::Composite do %i(created success pending) | false | 'running' | false %i(skipped success failed) | false | 'failed' | false %i(skipped success failed) | true | 'skipped' | false - %i(success manual) | true | 'pending' | false - %i(success failed created) | true | 'pending' | false + %i(success manual) | true | 'manual' | false + %i(success failed created) | true | 'running' | false end with_them do diff --git a/spec/lib/gitlab/ci/status/processable/waiting_for_resource_spec.rb b/spec/lib/gitlab/ci/status/processable/waiting_for_resource_spec.rb index 26087fd771c..e1baa1097e4 100644 --- a/spec/lib/gitlab/ci/status/processable/waiting_for_resource_spec.rb +++ b/spec/lib/gitlab/ci/status/processable/waiting_for_resource_spec.rb @@ -2,12 +2,25 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::Status::Processable::WaitingForResource do +RSpec.describe Gitlab::Ci::Status::Processable::WaitingForResource, feature_category: :continuous_integration do let(:user) { create(:user) } + let(:processable) { create(:ci_build, :waiting_for_resource, :resource_group) } - subject do - processable = create(:ci_build, :waiting_for_resource, :resource_group) - described_class.new(Gitlab::Ci::Status::Core.new(processable, user)) + subject { described_class.new(Gitlab::Ci::Status::Core.new(processable, user)) } + + it 'fabricates status with correct details' do + expect(subject.has_action?).to eq false + end + + context 'when resource is retained by a build' do + before do + processable.resource_group.assign_resource_to(create(:ci_build)) + end + + it 'fabricates status with correct details' do + expect(subject.has_action?).to eq true + expect(subject.action_path).to include 'jobs' + end end describe '#illustration' do diff --git a/spec/lib/gitlab/ci/templates/Jobs/build_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/Jobs/build_gitlab_ci_yaml_spec.rb index 07cfa939623..995922b6922 100644 --- a/spec/lib/gitlab/ci/templates/Jobs/build_gitlab_ci_yaml_spec.rb +++ b/spec/lib/gitlab/ci/templates/Jobs/build_gitlab_ci_yaml_spec.rb @@ -10,7 +10,7 @@ RSpec.describe 'Jobs/Build.gitlab-ci.yml' do describe 'AUTO_BUILD_IMAGE_VERSION' do it 'corresponds to a published image in the registry' do registry = "https://#{template_registry_host}" - repository = "gitlab-org/cluster-integration/auto-build-image" + repository = auto_build_image_repository reference = YAML.safe_load(template.content).dig('variables', 'AUTO_BUILD_IMAGE_VERSION') expect(public_image_exist?(registry, repository, reference)).to be true diff --git a/spec/lib/gitlab/ci/templates/Jobs/sast_iac_latest_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/Jobs/sast_iac_latest_gitlab_ci_yaml_spec.rb index 039a6a739dd..2b9213ea921 100644 --- a/spec/lib/gitlab/ci/templates/Jobs/sast_iac_latest_gitlab_ci_yaml_spec.rb +++ b/spec/lib/gitlab/ci/templates/Jobs/sast_iac_latest_gitlab_ci_yaml_spec.rb @@ -23,27 +23,33 @@ RSpec.describe 'Jobs/SAST-IaC.latest.gitlab-ci.yml', feature_category: :continuo allow(project).to receive(:default_branch).and_return(default_branch) end - context 'on feature branch' do - let(:pipeline_ref) { 'feature' } + context 'when SAST_DISABLED="false"' do + before do + create(:ci_variable, key: 'SAST_DISABLED', value: 'false', project: project) + end + + context 'on feature branch' do + let(:pipeline_ref) { 'feature' } - it 'creates the kics-iac-sast job' do - expect(build_names).to contain_exactly('kics-iac-sast') + it 'creates the kics-iac-sast job' do + expect(build_names).to contain_exactly('kics-iac-sast') + end end - end - context 'on merge request' do - let(:service) { MergeRequests::CreatePipelineService.new(project: project, current_user: user) } - let(:merge_request) { create(:merge_request, :simple, source_project: project) } - let(:pipeline) { service.execute(merge_request).payload } + context 'on merge request' do + let(:service) { MergeRequests::CreatePipelineService.new(project: project, current_user: user) } + let(:merge_request) { create(:merge_request, :simple, source_project: project) } + let(:pipeline) { service.execute(merge_request).payload } - it 'creates a pipeline with the expected jobs' do - expect(pipeline).to be_merge_request_event - expect(pipeline.errors.full_messages).to be_empty - expect(build_names).to match_array(%w(kics-iac-sast)) + it 'creates a pipeline with the expected jobs' do + expect(pipeline).to be_merge_request_event + expect(pipeline.errors.full_messages).to be_empty + expect(build_names).to match_array(%w(kics-iac-sast)) + end end end - context 'SAST_DISABLED is set' do + context 'when SAST_DISABLED="true"' do before do create(:ci_variable, key: 'SAST_DISABLED', value: 'true', project: project) end diff --git a/spec/lib/gitlab/ci/trace/chunked_io_spec.rb b/spec/lib/gitlab/ci/trace/chunked_io_spec.rb index 63625244fe8..7a926a06f16 100644 --- a/spec/lib/gitlab/ci/trace/chunked_io_spec.rb +++ b/spec/lib/gitlab/ci/trace/chunked_io_spec.rb @@ -446,15 +446,5 @@ RSpec.describe Gitlab::Ci::Trace::ChunkedIO, :clean_gitlab_redis_cache do expect(Ci::BuildTraceChunk.where(build: build).count).to eq(0) end - - context 'when the job does not have archived trace' do - it 'leaves a message in sidekiq log' do - expect(Sidekiq.logger).to receive(:warn).with( - message: 'The job does not have archived trace but going to be destroyed.', - job_id: build.id).and_call_original - - subject - end - end end end diff --git a/spec/lib/gitlab/ci/variables/builder/pipeline_spec.rb b/spec/lib/gitlab/ci/variables/builder/pipeline_spec.rb index a5365ae53b8..0a079a69682 100644 --- a/spec/lib/gitlab/ci/variables/builder/pipeline_spec.rb +++ b/spec/lib/gitlab/ci/variables/builder/pipeline_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::Variables::Builder::Pipeline, feature_category: :pipeline_authoring do +RSpec.describe Gitlab::Ci::Variables::Builder::Pipeline, feature_category: :secrets_management do let_it_be(:project) { create_default(:project, :repository, create_tag: 'test').freeze } let_it_be(:user) { create(:user) } @@ -30,15 +30,13 @@ RSpec.describe Gitlab::Ci::Variables::Builder::Pipeline, feature_category: :pipe CI_COMMIT_REF_PROTECTED CI_COMMIT_TIMESTAMP CI_COMMIT_AUTHOR - CI_BUILD_REF - CI_BUILD_BEFORE_SHA - CI_BUILD_REF_NAME - CI_BUILD_REF_SLUG ]) end - context 'when the pipeline is running for a tag' do - let(:pipeline) { build(:ci_empty_pipeline, :created, project: project, ref: 'test', tag: true) } + context 'when FF `ci_remove_legacy_predefined_variables` is disabled' do + before do + stub_feature_flags(ci_remove_legacy_predefined_variables: false) + end it 'includes all predefined variables in a valid order' do keys = subject.pluck(:key) @@ -52,6 +50,7 @@ RSpec.describe Gitlab::Ci::Variables::Builder::Pipeline, feature_category: :pipe CI_COMMIT_BEFORE_SHA CI_COMMIT_REF_NAME CI_COMMIT_REF_SLUG + CI_COMMIT_BRANCH CI_COMMIT_MESSAGE CI_COMMIT_TITLE CI_COMMIT_DESCRIPTION @@ -62,11 +61,69 @@ RSpec.describe Gitlab::Ci::Variables::Builder::Pipeline, feature_category: :pipe CI_BUILD_BEFORE_SHA CI_BUILD_REF_NAME CI_BUILD_REF_SLUG + ]) + end + end + + context 'when the pipeline is running for a tag' do + let(:pipeline) { build(:ci_empty_pipeline, :created, project: project, ref: 'test', tag: true) } + + it 'includes all predefined variables in a valid order' do + keys = subject.pluck(:key) + + expect(keys).to contain_exactly(*%w[ + CI_PIPELINE_IID + CI_PIPELINE_SOURCE + CI_PIPELINE_CREATED_AT + CI_COMMIT_SHA + CI_COMMIT_SHORT_SHA + CI_COMMIT_BEFORE_SHA + CI_COMMIT_REF_NAME + CI_COMMIT_REF_SLUG + CI_COMMIT_MESSAGE + CI_COMMIT_TITLE + CI_COMMIT_DESCRIPTION + CI_COMMIT_REF_PROTECTED + CI_COMMIT_TIMESTAMP + CI_COMMIT_AUTHOR CI_COMMIT_TAG CI_COMMIT_TAG_MESSAGE - CI_BUILD_TAG ]) end + + context 'when FF `ci_remove_legacy_predefined_variables` is disabled' do + before do + stub_feature_flags(ci_remove_legacy_predefined_variables: false) + end + + it 'includes all predefined variables in a valid order' do + keys = subject.pluck(:key) + + expect(keys).to contain_exactly(*%w[ + CI_PIPELINE_IID + CI_PIPELINE_SOURCE + CI_PIPELINE_CREATED_AT + CI_COMMIT_SHA + CI_COMMIT_SHORT_SHA + CI_COMMIT_BEFORE_SHA + CI_COMMIT_REF_NAME + CI_COMMIT_REF_SLUG + CI_COMMIT_MESSAGE + CI_COMMIT_TITLE + CI_COMMIT_DESCRIPTION + CI_COMMIT_REF_PROTECTED + CI_COMMIT_TIMESTAMP + CI_COMMIT_AUTHOR + CI_BUILD_REF + CI_BUILD_BEFORE_SHA + CI_BUILD_REF_NAME + CI_BUILD_REF_SLUG + CI_COMMIT_TAG + CI_COMMIT_TAG_MESSAGE + CI_BUILD_TAG + ]) + end + end end context 'when merge request is present' do @@ -305,10 +362,24 @@ RSpec.describe Gitlab::Ci::Variables::Builder::Pipeline, feature_category: :pipe expect(subject.to_hash.keys) .not_to include( 'CI_COMMIT_TAG', - 'CI_COMMIT_TAG_MESSAGE', - 'CI_BUILD_TAG' + 'CI_COMMIT_TAG_MESSAGE' ) end + + context 'when FF `ci_remove_legacy_predefined_variables` is disabled' do + before do + stub_feature_flags(ci_remove_legacy_predefined_variables: false) + end + + it 'does not expose tag variables' do + expect(subject.to_hash.keys) + .not_to include( + 'CI_COMMIT_TAG', + 'CI_COMMIT_TAG_MESSAGE', + 'CI_BUILD_TAG' + ) + end + end end context 'without a commit' do diff --git a/spec/lib/gitlab/ci/variables/builder_spec.rb b/spec/lib/gitlab/ci/variables/builder_spec.rb index bbd3dc54e6a..10974993fa4 100644 --- a/spec/lib/gitlab/ci/variables/builder_spec.rb +++ b/spec/lib/gitlab/ci/variables/builder_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::Variables::Builder, :clean_gitlab_redis_cache, feature_category: :pipeline_authoring do +RSpec.describe Gitlab::Ci::Variables::Builder, :clean_gitlab_redis_cache, feature_category: :secrets_management do include Ci::TemplateHelpers let_it_be(:group) { create(:group) } let_it_be(:project) { create(:project, :repository, namespace: group) } @@ -35,10 +35,6 @@ RSpec.describe Gitlab::Ci::Variables::Builder, :clean_gitlab_redis_cache, featur value: '1' }, { key: 'CI_ENVIRONMENT_NAME', value: 'test' }, - { key: 'CI_BUILD_NAME', - value: 'rspec:test 1' }, - { key: 'CI_BUILD_STAGE', - value: job.stage_name }, { key: 'CI', value: 'true' }, { key: 'GITLAB_CI', @@ -51,6 +47,10 @@ RSpec.describe Gitlab::Ci::Variables::Builder, :clean_gitlab_redis_cache, featur value: Gitlab.config.gitlab.port.to_s }, { key: 'CI_SERVER_PROTOCOL', value: Gitlab.config.gitlab.protocol }, + { key: 'CI_SERVER_SHELL_SSH_HOST', + value: Gitlab.config.gitlab_shell.ssh_host.to_s }, + { key: 'CI_SERVER_SHELL_SSH_PORT', + value: Gitlab.config.gitlab_shell.ssh_port.to_s }, { key: 'CI_SERVER_NAME', value: 'GitLab' }, { key: 'CI_SERVER_VERSION', @@ -101,6 +101,8 @@ RSpec.describe Gitlab::Ci::Variables::Builder, :clean_gitlab_redis_cache, featur value: project.pages_url }, { key: 'CI_API_V4_URL', value: API::Helpers::Version.new('v4').root_url }, + { key: 'CI_API_GRAPHQL_URL', + value: Gitlab::Routing.url_helpers.api_graphql_url }, { key: 'CI_TEMPLATE_REGISTRY_HOST', value: template_registry_host }, { key: 'CI_PIPELINE_IID', @@ -133,14 +135,6 @@ RSpec.describe Gitlab::Ci::Variables::Builder, :clean_gitlab_redis_cache, featur value: pipeline.git_commit_timestamp }, { key: 'CI_COMMIT_AUTHOR', value: pipeline.git_author_full_text }, - { key: 'CI_BUILD_REF', - value: job.sha }, - { key: 'CI_BUILD_BEFORE_SHA', - value: job.before_sha }, - { key: 'CI_BUILD_REF_NAME', - value: job.ref }, - { key: 'CI_BUILD_REF_SLUG', - value: job.ref_slug }, { key: 'YAML_VARIABLE', value: 'value' }, { key: 'GITLAB_USER_ID', @@ -160,6 +154,151 @@ RSpec.describe Gitlab::Ci::Variables::Builder, :clean_gitlab_redis_cache, featur it { expect(subject.to_runner_variables).to eq(predefined_variables) } + context 'when FF `ci_remove_legacy_predefined_variables` is disabled' do + before do + stub_feature_flags(ci_remove_legacy_predefined_variables: false) + end + + let(:predefined_variables) do + [ + { key: 'CI_JOB_NAME', + value: 'rspec:test 1' }, + { key: 'CI_JOB_NAME_SLUG', + value: 'rspec-test-1' }, + { key: 'CI_JOB_STAGE', + value: job.stage_name }, + { key: 'CI_NODE_TOTAL', + value: '1' }, + { key: 'CI_ENVIRONMENT_NAME', + value: 'test' }, + { key: 'CI_BUILD_NAME', + value: 'rspec:test 1' }, + { key: 'CI_BUILD_STAGE', + value: job.stage_name }, + { key: 'CI', + value: 'true' }, + { key: 'GITLAB_CI', + value: 'true' }, + { key: 'CI_SERVER_URL', + value: Gitlab.config.gitlab.url }, + { key: 'CI_SERVER_HOST', + value: Gitlab.config.gitlab.host }, + { key: 'CI_SERVER_PORT', + value: Gitlab.config.gitlab.port.to_s }, + { key: 'CI_SERVER_PROTOCOL', + value: Gitlab.config.gitlab.protocol }, + { key: 'CI_SERVER_SHELL_SSH_HOST', + value: Gitlab.config.gitlab_shell.ssh_host.to_s }, + { key: 'CI_SERVER_SHELL_SSH_PORT', + value: Gitlab.config.gitlab_shell.ssh_port.to_s }, + { key: 'CI_SERVER_NAME', + value: 'GitLab' }, + { key: 'CI_SERVER_VERSION', + value: Gitlab::VERSION }, + { key: 'CI_SERVER_VERSION_MAJOR', + value: Gitlab.version_info.major.to_s }, + { key: 'CI_SERVER_VERSION_MINOR', + value: Gitlab.version_info.minor.to_s }, + { key: 'CI_SERVER_VERSION_PATCH', + value: Gitlab.version_info.patch.to_s }, + { key: 'CI_SERVER_REVISION', + value: Gitlab.revision }, + { key: 'GITLAB_FEATURES', + value: project.licensed_features.join(',') }, + { key: 'CI_PROJECT_ID', + value: project.id.to_s }, + { key: 'CI_PROJECT_NAME', + value: project.path }, + { key: 'CI_PROJECT_TITLE', + value: project.title }, + { key: 'CI_PROJECT_DESCRIPTION', + value: project.description }, + { key: 'CI_PROJECT_PATH', + value: project.full_path }, + { key: 'CI_PROJECT_PATH_SLUG', + value: project.full_path_slug }, + { key: 'CI_PROJECT_NAMESPACE', + value: project.namespace.full_path }, + { key: 'CI_PROJECT_NAMESPACE_ID', + value: project.namespace.id.to_s }, + { key: 'CI_PROJECT_ROOT_NAMESPACE', + value: project.namespace.root_ancestor.path }, + { key: 'CI_PROJECT_URL', + value: project.web_url }, + { key: 'CI_PROJECT_VISIBILITY', + value: "private" }, + { key: 'CI_PROJECT_REPOSITORY_LANGUAGES', + value: project.repository_languages.map(&:name).join(',').downcase }, + { key: 'CI_PROJECT_CLASSIFICATION_LABEL', + value: project.external_authorization_classification_label }, + { key: 'CI_DEFAULT_BRANCH', + value: project.default_branch }, + { key: 'CI_CONFIG_PATH', + value: project.ci_config_path_or_default }, + { key: 'CI_PAGES_DOMAIN', + value: Gitlab.config.pages.host }, + { key: 'CI_PAGES_URL', + value: project.pages_url }, + { key: 'CI_API_V4_URL', + value: API::Helpers::Version.new('v4').root_url }, + { key: 'CI_API_GRAPHQL_URL', + value: Gitlab::Routing.url_helpers.api_graphql_url }, + { key: 'CI_TEMPLATE_REGISTRY_HOST', + value: template_registry_host }, + { key: 'CI_PIPELINE_IID', + value: pipeline.iid.to_s }, + { key: 'CI_PIPELINE_SOURCE', + value: pipeline.source }, + { key: 'CI_PIPELINE_CREATED_AT', + value: pipeline.created_at.iso8601 }, + { key: 'CI_COMMIT_SHA', + value: job.sha }, + { key: 'CI_COMMIT_SHORT_SHA', + value: job.short_sha }, + { key: 'CI_COMMIT_BEFORE_SHA', + value: job.before_sha }, + { key: 'CI_COMMIT_REF_NAME', + value: job.ref }, + { key: 'CI_COMMIT_REF_SLUG', + value: job.ref_slug }, + { key: 'CI_COMMIT_BRANCH', + value: job.ref }, + { key: 'CI_COMMIT_MESSAGE', + value: pipeline.git_commit_message }, + { key: 'CI_COMMIT_TITLE', + value: pipeline.git_commit_title }, + { key: 'CI_COMMIT_DESCRIPTION', + value: pipeline.git_commit_description }, + { key: 'CI_COMMIT_REF_PROTECTED', + value: (!!pipeline.protected_ref?).to_s }, + { key: 'CI_COMMIT_TIMESTAMP', + value: pipeline.git_commit_timestamp }, + { key: 'CI_COMMIT_AUTHOR', + value: pipeline.git_author_full_text }, + { key: 'CI_BUILD_REF', + value: job.sha }, + { key: 'CI_BUILD_BEFORE_SHA', + value: job.before_sha }, + { key: 'CI_BUILD_REF_NAME', + value: job.ref }, + { key: 'CI_BUILD_REF_SLUG', + value: job.ref_slug }, + { key: 'YAML_VARIABLE', + value: 'value' }, + { key: 'GITLAB_USER_ID', + value: user.id.to_s }, + { key: 'GITLAB_USER_EMAIL', + value: user.email }, + { key: 'GITLAB_USER_LOGIN', + value: user.username }, + { key: 'GITLAB_USER_NAME', + value: user.name } + ].map { |var| var.merge(public: true, masked: false) } + end + + it { expect(subject.to_runner_variables).to eq(predefined_variables) } + end + context 'variables ordering' do def var(name, value) { key: name, value: value.to_s, public: true, masked: false } diff --git a/spec/lib/gitlab/ci/variables/collection_spec.rb b/spec/lib/gitlab/ci/variables/collection_spec.rb index 4ee122cc607..181e37de9b9 100644 --- a/spec/lib/gitlab/ci/variables/collection_spec.rb +++ b/spec/lib/gitlab/ci/variables/collection_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::Variables::Collection, feature_category: :pipeline_authoring do +RSpec.describe Gitlab::Ci::Variables::Collection, feature_category: :secrets_management do describe '.new' do it 'can be initialized with an array' do variable = { key: 'VAR', value: 'value', public: true, masked: false } diff --git a/spec/lib/gitlab/ci/yaml_processor/result_spec.rb b/spec/lib/gitlab/ci/yaml_processor/result_spec.rb index 5c9f156e054..36ada9050b2 100644 --- a/spec/lib/gitlab/ci/yaml_processor/result_spec.rb +++ b/spec/lib/gitlab/ci/yaml_processor/result_spec.rb @@ -47,8 +47,8 @@ module Gitlab end it 'returns expanded yaml config' do - expanded_config = YAML.safe_load(config_metadata[:merged_yaml], [Symbol]) - included_config = YAML.safe_load(included_yml, [Symbol]) + expanded_config = YAML.safe_load(config_metadata[:merged_yaml], permitted_classes: [Symbol]) + included_config = YAML.safe_load(included_yml, permitted_classes: [Symbol]) expect(expanded_config).to include(*included_config.keys) end diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb index 360686ce65c..2c020e76cb6 100644 --- a/spec/lib/gitlab/ci/yaml_processor_spec.rb +++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb @@ -4,7 +4,7 @@ require 'spec_helper' module Gitlab module Ci - RSpec.describe YamlProcessor, feature_category: :pipeline_authoring do + RSpec.describe YamlProcessor, feature_category: :pipeline_composition do include StubRequests include RepoHelpers @@ -659,6 +659,191 @@ module Gitlab it_behaves_like 'has warnings and expected error', /build job: need test is not defined in current or prior stages/ end + + describe '#validate_job_needs!' do + context "when all validations pass" do + let(:config) do + <<-EOYML + stages: + - lint + lint_job: + needs: [lint_job_2] + stage: lint + script: 'echo lint_job' + rules: + - if: $var == null + needs: + - lint_job_2 + - job: lint_job_3 + optional: true + lint_job_2: + stage: lint + script: 'echo job' + rules: + - if: $var == null + lint_job_3: + stage: lint + script: 'echo job' + rules: + - if: $var == null + EOYML + end + + it 'returns a valid response' do + expect(subject).to be_valid + expect(subject).to be_instance_of(Gitlab::Ci::YamlProcessor::Result) + end + end + + context 'needs as array' do + context 'single need in following stage' do + let(:config) do + <<-EOYML + stages: + - lint + - test + lint_job: + stage: lint + script: 'echo lint_job' + rules: + - if: $var == null + needs: [test_job] + test_job: + stage: test + script: 'echo job' + rules: + - if: $var == null + EOYML + end + + it_behaves_like 'returns errors', 'lint_job job: need test_job is not defined in current or prior stages' + end + + context 'multiple needs in the following stage' do + let(:config) do + <<-EOYML + stages: + - lint + - test + lint_job: + stage: lint + script: 'echo lint_job' + rules: + - if: $var == null + needs: [test_job, test_job_2] + test_job: + stage: test + script: 'echo job' + rules: + - if: $var == null + test_job_2: + stage: test + script: 'echo job' + rules: + - if: $var == null + EOYML + end + + it_behaves_like 'returns errors', 'lint_job job: need test_job is not defined in current or prior stages' + end + + context 'single need in following state - hyphen need' do + let(:config) do + <<-EOYML + stages: + - lint + - test + lint_job: + stage: lint + script: 'echo lint_job' + rules: + - if: $var == null + needs: + - test_job + test_job: + stage: test + script: 'echo job' + rules: + - if: $var == null + EOYML + end + + it_behaves_like 'returns errors', 'lint_job job: need test_job is not defined in current or prior stages' + end + + context 'when there are duplicate needs (string and hash)' do + let(:config) do + <<-EOYML + stages: + - test + test_job_1: + stage: test + script: 'echo lint_job' + rules: + - if: $var == null + needs: + - test_job_2 + - job: test_job_2 + test_job_2: + stage: test + script: 'echo job' + rules: + - if: $var == null + EOYML + end + + it_behaves_like 'returns errors', 'test_job_1 has the following needs duplicated: test_job_2.' + end + end + + context 'rule needs as hash' do + context 'single hash need in following stage' do + let(:config) do + <<-EOYML + stages: + - lint + - test + lint_job: + stage: lint + script: 'echo lint_job' + rules: + - if: $var == null + needs: + - job: test_job + artifacts: false + optional: false + test_job: + stage: test + script: 'echo job' + rules: + - if: $var == null + EOYML + end + + it_behaves_like 'returns errors', 'lint_job job: need test_job is not defined in current or prior stages' + end + end + + context 'job rule need does not exist' do + let(:config) do + <<-EOYML + build: + stage: build + script: echo + rules: + - when: always + test: + stage: test + script: echo + rules: + - if: $var == null + needs: [unknown_job] + EOYML + end + + it_behaves_like 'has warnings and expected error', /test job: undefined need: unknown_job/ + end + end end end @@ -1685,7 +1870,8 @@ module Gitlab key: 'key', policy: 'pull-push', when: 'on_success', - unprotect: false + unprotect: false, + fallback_keys: [] ]) end @@ -1710,7 +1896,8 @@ module Gitlab key: { files: ['file'] }, policy: 'pull-push', when: 'on_success', - unprotect: false + unprotect: false, + fallback_keys: [] ]) end @@ -1737,7 +1924,8 @@ module Gitlab key: 'keya', policy: 'pull-push', when: 'on_success', - unprotect: false + unprotect: false, + fallback_keys: [] }, { paths: ['logs/', 'binaries/'], @@ -1745,7 +1933,8 @@ module Gitlab key: 'key', policy: 'pull-push', when: 'on_success', - unprotect: false + unprotect: false, + fallback_keys: [] } ] ) @@ -1773,7 +1962,8 @@ module Gitlab key: { files: ['file'] }, policy: 'pull-push', when: 'on_success', - unprotect: false + unprotect: false, + fallback_keys: [] ]) end @@ -1799,7 +1989,8 @@ module Gitlab key: { files: ['file'], prefix: 'prefix' }, policy: 'pull-push', when: 'on_success', - unprotect: false + unprotect: false, + fallback_keys: [] ]) end @@ -1823,7 +2014,8 @@ module Gitlab key: 'local', policy: 'pull-push', when: 'on_success', - unprotect: false + unprotect: false, + fallback_keys: [] ]) end end @@ -2395,10 +2587,16 @@ module Gitlab end end - context 'undefined need' do + context 'when need is an undefined job' do let(:needs) { ['undefined'] } it_behaves_like 'returns errors', 'test1 job: undefined need: undefined' + + context 'when need is optional' do + let(:needs) { [{ job: 'undefined', optional: true }] } + + it { is_expected.to be_valid } + end end context 'needs to deploy' do @@ -2408,9 +2606,33 @@ module Gitlab end context 'duplicate needs' do - let(:needs) { %w(build1 build1) } + context 'when needs are specified in an array' do + let(:needs) { %w(build1 build1) } + + it_behaves_like 'returns errors', 'test1 has the following needs duplicated: build1.' + end + + context 'when a job is specified multiple times' do + let(:needs) do + [ + { job: "build2", artifacts: true, optional: false }, + { job: "build2", artifacts: true, optional: false } + ] + end - it_behaves_like 'returns errors', 'test1 has duplicate entries in the needs section.' + it_behaves_like 'returns errors', 'test1 has the following needs duplicated: build2.' + end + + context 'when job is specified multiple times with different attributes' do + let(:needs) do + [ + { job: "build2", artifacts: false, optional: true }, + { job: "build2", artifacts: true, optional: false } + ] + end + + it_behaves_like 'returns errors', 'test1 has the following needs duplicated: build2.' + end end context 'needs and dependencies that are mismatching' do |