summaryrefslogtreecommitdiff
path: root/spec/lib/gitlab/ci
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-05-17 16:05:49 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2023-05-17 16:05:49 +0000
commit43a25d93ebdabea52f99b05e15b06250cd8f07d7 (patch)
treedceebdc68925362117480a5d672bcff122fb625b /spec/lib/gitlab/ci
parent20c84b99005abd1c82101dfeff264ac50d2df211 (diff)
downloadgitlab-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')
-rw-r--r--spec/lib/gitlab/ci/ansi2json/state_spec.rb68
-rw-r--r--spec/lib/gitlab/ci/ansi2json_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/badge/release/template_spec.rb23
-rw-r--r--spec/lib/gitlab/ci/build/auto_retry_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/build/cache_spec.rb47
-rw-r--r--spec/lib/gitlab/ci/build/context/build_spec.rb25
-rw-r--r--spec/lib/gitlab/ci/build/context/global_spec.rb11
-rw-r--r--spec/lib/gitlab/ci/build/hook_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/build/rules_spec.rb144
-rw-r--r--spec/lib/gitlab/ci/components/instance_path_spec.rb35
-rw-r--r--spec/lib/gitlab/ci/config/entry/cache_spec.rb26
-rw-r--r--spec/lib/gitlab/ci/config/entry/job_spec.rb78
-rw-r--r--spec/lib/gitlab/ci/config/entry/policy_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/config/entry/processable_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/config/entry/product/parallel_spec.rb6
-rw-r--r--spec/lib/gitlab/ci/config/entry/publish_spec.rb40
-rw-r--r--spec/lib/gitlab/ci/config/entry/pull_policy_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/config/entry/reports/coverage_report_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/config/entry/reports_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/config/entry/root_spec.rb18
-rw-r--r--spec/lib/gitlab/ci/config/entry/trigger_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/config/external/context_spec.rb70
-rw-r--r--spec/lib/gitlab/ci/config/external/file/artifact_spec.rb43
-rw-r--r--spec/lib/gitlab/ci/config/external/file/base_spec.rb133
-rw-r--r--spec/lib/gitlab/ci/config/external/file/component_spec.rb35
-rw-r--r--spec/lib/gitlab/ci/config/external/file/local_spec.rb30
-rw-r--r--spec/lib/gitlab/ci/config/external/file/project_spec.rb71
-rw-r--r--spec/lib/gitlab/ci/config/external/file/remote_spec.rb41
-rw-r--r--spec/lib/gitlab/ci/config/external/file/template_spec.rb35
-rw-r--r--spec/lib/gitlab/ci/config/external/interpolator_spec.rb319
-rw-r--r--spec/lib/gitlab/ci/config/external/mapper/base_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/config/external/mapper/filter_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/config/external/mapper/location_expander_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/config/external/mapper/matcher_spec.rb68
-rw-r--r--spec/lib/gitlab/ci/config/external/mapper/normalizer_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/config/external/mapper/variables_expander_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/config/external/mapper/verifier_spec.rb275
-rw-r--r--spec/lib/gitlab/ci/config/external/mapper_spec.rb13
-rw-r--r--spec/lib/gitlab/ci/config/external/processor_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/config/external/rules_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/config/header/input_spec.rb70
-rw-r--r--spec/lib/gitlab/ci/config/header/root_spec.rb133
-rw-r--r--spec/lib/gitlab/ci/config/header/spec_spec.rb56
-rw-r--r--spec/lib/gitlab/ci/config/normalizer/number_strategy_spec.rb22
-rw-r--r--spec/lib/gitlab/ci/config/yaml/result_spec.rb54
-rw-r--r--spec/lib/gitlab/ci/config/yaml_spec.rb159
-rw-r--r--spec/lib/gitlab/ci/config_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/input/arguments/base_spec.rb19
-rw-r--r--spec/lib/gitlab/ci/input/arguments/default_spec.rb53
-rw-r--r--spec/lib/gitlab/ci/input/arguments/options_spec.rb54
-rw-r--r--spec/lib/gitlab/ci/input/arguments/required_spec.rb45
-rw-r--r--spec/lib/gitlab/ci/input/arguments/unknown_spec.rb18
-rw-r--r--spec/lib/gitlab/ci/input/inputs_spec.rb126
-rw-r--r--spec/lib/gitlab/ci/interpolation/access_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/interpolation/block_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/interpolation/config_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/interpolation/context_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/interpolation/template_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/jwt_spec.rb17
-rw-r--r--spec/lib/gitlab/ci/jwt_v2_spec.rb78
-rw-r--r--spec/lib/gitlab/ci/lint_spec.rb6
-rw-r--r--spec/lib/gitlab/ci/parsers/security/common_spec.rb8
-rw-r--r--spec/lib/gitlab/ci/parsers/security/sast_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/parsers/security/secret_detection_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/parsers/security/validators/schema_validator_spec.rb190
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/config/content_spec.rb11
-rw-r--r--spec/lib/gitlab/ci/pipeline/duration_spec.rb156
-rw-r--r--spec/lib/gitlab/ci/pipeline/seed/build/cache_spec.rb48
-rw-r--r--spec/lib/gitlab/ci/pipeline/seed/build_spec.rb128
-rw-r--r--spec/lib/gitlab/ci/pipeline/seed/stage_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/project_config/repository_spec.rb8
-rw-r--r--spec/lib/gitlab/ci/project_config/source_spec.rb8
-rw-r--r--spec/lib/gitlab/ci/reports/codequality_mr_diff_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/reports/security/scanner_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/reports/security/vulnerability_reports_comparer_spec.rb163
-rw-r--r--spec/lib/gitlab/ci/runner_releases_spec.rb32
-rw-r--r--spec/lib/gitlab/ci/secure_files/cer_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/secure_files/mobile_provision_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/secure_files/p12_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/status/composite_spec.rb18
-rw-r--r--spec/lib/gitlab/ci/status/processable/waiting_for_resource_spec.rb21
-rw-r--r--spec/lib/gitlab/ci/templates/Jobs/build_gitlab_ci_yaml_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/templates/Jobs/sast_iac_latest_gitlab_ci_yaml_spec.rb34
-rw-r--r--spec/lib/gitlab/ci/trace/chunked_io_spec.rb10
-rw-r--r--spec/lib/gitlab/ci/variables/builder/pipeline_spec.rb91
-rw-r--r--spec/lib/gitlab/ci/variables/builder_spec.rb165
-rw-r--r--spec/lib/gitlab/ci/variables/collection_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/yaml_processor/result_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/yaml_processor_spec.rb244
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