diff options
Diffstat (limited to 'spec/lib/gitlab/ci')
49 files changed, 1776 insertions, 337 deletions
diff --git a/spec/lib/gitlab/ci/ansi2html_spec.rb b/spec/lib/gitlab/ci/ansi2html_spec.rb index bf1f2bae7da..27c2b005a93 100644 --- a/spec/lib/gitlab/ci/ansi2html_spec.rb +++ b/spec/lib/gitlab/ci/ansi2html_spec.rb @@ -150,6 +150,10 @@ RSpec.describe Gitlab::Ci::Ansi2html do expect(convert_html("\r\n")).to eq('<span><br/></span>') end + it 'replaces invalid UTF-8 data' do + expect(convert_html("UTF-8 dashes here: ───\n🐤🐤🐤🐤\xF0\x9F\x90\n")).to eq("<span>UTF-8 dashes here: ───<br/>🐤🐤🐤🐤�<br/></span>") + end + describe "incremental update" do shared_examples 'stateable converter' do let(:pass1_stream) { StringIO.new(pre_text) } diff --git a/spec/lib/gitlab/ci/build/auto_retry_spec.rb b/spec/lib/gitlab/ci/build/auto_retry_spec.rb index b107553bbce..e83e1326206 100644 --- a/spec/lib/gitlab/ci/build/auto_retry_spec.rb +++ b/spec/lib/gitlab/ci/build/auto_retry_spec.rb @@ -53,24 +53,8 @@ RSpec.describe Gitlab::Ci::Build::AutoRetry do context 'with retries max config option' do let(:build) { create(:ci_build, options: { retry: { max: 1 } }) } - context 'when build_metadata_config is set' do - before do - stub_feature_flags(ci_build_metadata_config: true) - end - - it 'returns the number of configured max retries' do - expect(result).to eq 1 - end - end - - context 'when build_metadata_config is not set' do - before do - stub_feature_flags(ci_build_metadata_config: false) - end - - it 'returns the number of configured max retries' do - expect(result).to eq 1 - end + it 'returns the number of configured max retries' do + expect(result).to eq 1 end end diff --git a/spec/lib/gitlab/ci/config/entry/bridge_spec.rb b/spec/lib/gitlab/ci/config/entry/bridge_spec.rb index d294eca7f15..6c9c8fa5df5 100644 --- a/spec/lib/gitlab/ci/config/entry/bridge_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/bridge_spec.rb @@ -106,7 +106,6 @@ RSpec.describe Gitlab::Ci::Config::Entry::Bridge do ignore: false, stage: 'test', only: { refs: %w[branches tags] }, - variables: {}, job_variables: {}, root_variables_inheritance: true, scheduling_type: :stage) @@ -131,7 +130,6 @@ RSpec.describe Gitlab::Ci::Config::Entry::Bridge do ignore: false, stage: 'test', only: { refs: %w[branches tags] }, - variables: {}, job_variables: {}, root_variables_inheritance: true, scheduling_type: :stage) @@ -287,7 +285,6 @@ RSpec.describe Gitlab::Ci::Config::Entry::Bridge do only: { refs: %w[branches tags] }, parallel: { matrix: [{ 'PROVIDER' => ['aws'], 'STACK' => %w(monitoring app1) }, { 'PROVIDER' => ['gcp'], 'STACK' => %w(data) }] }, - variables: {}, job_variables: {}, root_variables_inheritance: true, scheduling_type: :stage diff --git a/spec/lib/gitlab/ci/config/entry/include/rules/rule_spec.rb b/spec/lib/gitlab/ci/config/entry/include/rules/rule_spec.rb new file mode 100644 index 00000000000..b99048e2c18 --- /dev/null +++ b/spec/lib/gitlab/ci/config/entry/include/rules/rule_spec.rb @@ -0,0 +1,90 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' + +RSpec.describe Gitlab::Ci::Config::Entry::Include::Rules::Rule do + let(:factory) do + Gitlab::Config::Entry::Factory.new(described_class) + .value(config) + end + + subject(:entry) { factory.create! } + + describe '.new' do + shared_examples 'an invalid config' do |error_message| + it { is_expected.not_to be_valid } + + it 'has errors' do + expect(entry.errors).to include(error_message) + end + end + + context 'when specifying an if: clause' do + let(:config) { { if: '$THIS || $THAT' } } + + it { is_expected.to be_valid } + end + + context 'using a list of multiple expressions' do + let(:config) { { if: ['$MY_VAR == "this"', '$YOUR_VAR == "that"'] } } + + it_behaves_like 'an invalid config', /invalid expression syntax/ + end + + context 'when specifying an invalid if: clause expression' do + let(:config) { { if: ['$MY_VAR =='] } } + + it_behaves_like 'an invalid config', /invalid expression syntax/ + end + + context 'when specifying an if: clause expression with an invalid token' do + let(:config) { { if: ['$MY_VAR == 123'] } } + + it_behaves_like 'an invalid config', /invalid expression syntax/ + end + + context 'when using invalid regex in an if: clause' do + let(:config) { { if: ['$MY_VAR =~ /some ( thing/'] } } + + it_behaves_like 'an invalid config', /invalid expression syntax/ + end + + context 'when using an if: clause with lookahead regex character "?"' do + let(:config) { { if: '$CI_COMMIT_REF =~ /^(?!master).+/' } } + + context 'when allow_unsafe_ruby_regexp is disabled' do + it_behaves_like 'an invalid config', /invalid expression syntax/ + end + end + + context 'when specifying unknown policy' do + let(:config) { { invalid: :something } } + + it_behaves_like 'an invalid config', /unknown keys: invalid/ + end + + context 'when clause is empty' do + let(:config) { {} } + + it_behaves_like 'an invalid config', /can't be blank/ + end + + context 'when policy strategy does not match' do + let(:config) { 'string strategy' } + + it_behaves_like 'an invalid config', /should be a hash/ + end + end + + describe '#value' do + subject(:value) { entry.value } + + context 'when specifying an if: clause' do + let(:config) { { if: '$THIS || $THAT' } } + + it 'returns the config' do + expect(subject).to eq(if: '$THIS || $THAT') + end + end + end +end diff --git a/spec/lib/gitlab/ci/config/entry/include/rules_spec.rb b/spec/lib/gitlab/ci/config/entry/include/rules_spec.rb new file mode 100644 index 00000000000..c255d6e9dd6 --- /dev/null +++ b/spec/lib/gitlab/ci/config/entry/include/rules_spec.rb @@ -0,0 +1,98 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' + +RSpec.describe Gitlab::Ci::Config::Entry::Include::Rules do + let(:factory) do + Gitlab::Config::Entry::Factory.new(described_class) + .value(config) + end + + subject(:entry) { factory.create! } + + describe '.new' do + shared_examples 'a valid config' do + it { is_expected.to be_valid } + + context 'when composed' do + before do + entry.compose! + end + + it { is_expected.to be_valid } + end + end + + shared_examples 'an invalid config' do |error_message| + it { is_expected.not_to be_valid } + + it 'has errors' do + expect(entry.errors).to include(error_message) + end + end + + context 'with an "if"' do + let(:config) do + [{ if: '$THIS == "that"' }] + end + + it_behaves_like 'a valid config' + end + + context 'with a "changes"' do + let(:config) do + [{ changes: ['filename.txt'] }] + end + + context 'when composed' do + before do + entry.compose! + end + + it_behaves_like 'an invalid config', /contains unknown keys: changes/ + end + end + + context 'with a list of two rules' do + let(:config) do + [ + { if: '$THIS == "that"' }, + { if: '$SKIP' } + ] + end + + it_behaves_like 'a valid config' + end + + context 'without an array' do + let(:config) do + { if: '$SKIP' } + end + + it_behaves_like 'an invalid config', /should be a array/ + end + end + + describe '#value' do + subject(:value) { entry.value } + + context 'with an "if"' do + let(:config) do + [{ if: '$THIS == "that"' }] + end + + it { is_expected.to eq(config) } + end + + context 'with a list of two rules' do + let(:config) do + [ + { if: '$THIS == "that"' }, + { if: '$SKIP' } + ] + end + + it { is_expected.to eq(config) } + end + end +end diff --git a/spec/lib/gitlab/ci/config/entry/include_spec.rb b/spec/lib/gitlab/ci/config/entry/include_spec.rb index 59f0b0e7a48..275cdcddeb0 100644 --- a/spec/lib/gitlab/ci/config/entry/include_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/include_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' +require 'fast_spec_helper' RSpec.describe ::Gitlab::Ci::Config::Entry::Include do subject(:include_entry) { described_class.new(config) } @@ -86,6 +86,22 @@ RSpec.describe ::Gitlab::Ci::Config::Entry::Include do end end end + + context 'when using with "rules"' do + let(:config) { { local: 'test.yml', rules: [{ if: '$VARIABLE' }] } } + + it { is_expected.to be_valid } + + context 'when rules is not an array of hashes' do + let(:config) { { local: 'test.yml', rules: ['$VARIABLE'] } } + + it { is_expected.not_to be_valid } + + it 'has specific error' do + expect(include_entry.errors).to include('include rules should be an array of hashes') + end + end + end end context 'when value is something else' do @@ -94,4 +110,26 @@ RSpec.describe ::Gitlab::Ci::Config::Entry::Include do it { is_expected.not_to be_valid } end end + + describe '#value' do + subject(:value) { include_entry.value } + + context 'when config is a string' do + let(:config) { 'test.yml' } + + it { is_expected.to eq('test.yml') } + end + + context 'when config is a hash' do + let(:config) { { local: 'test.yml' } } + + it { is_expected.to eq(local: 'test.yml') } + end + + context 'when config has "rules"' do + let(:config) { { local: 'test.yml', rules: [{ if: '$VARIABLE' }] } } + + it { is_expected.to eq(local: 'test.yml', rules: [{ if: '$VARIABLE' }]) } + end + end end diff --git a/spec/lib/gitlab/ci/config/entry/inherit/variables_spec.rb b/spec/lib/gitlab/ci/config/entry/inherit/variables_spec.rb index b1a8fbcdbe0..bdb4d25c142 100644 --- a/spec/lib/gitlab/ci/config/entry/inherit/variables_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/inherit/variables_spec.rb @@ -24,19 +24,4 @@ RSpec.describe ::Gitlab::Ci::Config::Entry::Inherit::Variables do end end end - - describe '#inherit?' do - where(:config, :inherit) do - true | true - false | false - %w[A] | true - %w[B] | false - end - - with_them do - it do - expect(subject.inherit?('A')).to eq(inherit) - end - end - end end diff --git a/spec/lib/gitlab/ci/config/entry/job_spec.rb b/spec/lib/gitlab/ci/config/entry/job_spec.rb index 1d23ab0c2c7..5b47d3a3922 100644 --- a/spec/lib/gitlab/ci/config/entry/job_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/job_spec.rb @@ -434,20 +434,6 @@ RSpec.describe Gitlab::Ci::Config::Entry::Job do expect(entry.errors).to include 'job dependencies the another-job should be part of needs' end end - - context 'when stage: is missing' do - let(:config) do - { - script: 'echo', - needs: ['build-job'] - } - end - - it 'returns error about invalid data' do - expect(entry).not_to be_valid - expect(entry.errors).to include 'job config missing required keys: stage' - end - end end context 'when timeout value is not correct' do @@ -626,7 +612,6 @@ RSpec.describe Gitlab::Ci::Config::Entry::Job do ignore: false, after_script: %w[cleanup], only: { refs: %w[branches tags] }, - variables: {}, job_variables: {}, root_variables_inheritance: true, scheduling_type: :stage) diff --git a/spec/lib/gitlab/ci/config/entry/jobs_spec.rb b/spec/lib/gitlab/ci/config/entry/jobs_spec.rb index cb73044b62b..9a2a67389fc 100644 --- a/spec/lib/gitlab/ci/config/entry/jobs_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/jobs_spec.rb @@ -99,7 +99,6 @@ RSpec.describe Gitlab::Ci::Config::Entry::Jobs do only: { refs: %w[branches tags] }, stage: 'test', trigger: { project: 'my/project' }, - variables: {}, job_variables: {}, root_variables_inheritance: true, scheduling_type: :stage @@ -110,7 +109,6 @@ RSpec.describe Gitlab::Ci::Config::Entry::Jobs do only: { refs: %w[branches tags] }, script: ['something'], stage: 'test', - variables: {}, job_variables: {}, root_variables_inheritance: true, scheduling_type: :stage diff --git a/spec/lib/gitlab/ci/config/entry/processable_spec.rb b/spec/lib/gitlab/ci/config/entry/processable_spec.rb index f98a6a869d6..b872f6644a2 100644 --- a/spec/lib/gitlab/ci/config/entry/processable_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/processable_spec.rb @@ -362,76 +362,6 @@ RSpec.describe Gitlab::Ci::Config::Entry::Processable do end context 'with inheritance' do - context 'of variables' do - let(:config) do - { variables: { A: 'job', B: 'job' } } - end - - before do - entry.compose!(deps) - end - - context 'with only job variables' do - it 'does return defined variables' do - expect(entry.value).to include( - variables: { 'A' => 'job', 'B' => 'job' }, - job_variables: { 'A' => 'job', 'B' => 'job' }, - root_variables_inheritance: true - ) - end - end - - context 'when root yaml variables are used' do - let(:variables) do - Gitlab::Ci::Config::Entry::Variables.new( - { A: 'root', C: 'root', D: 'root' } - ).value - end - - it 'does return job and root variables' do - expect(entry.value).to include( - variables: { 'A' => 'job', 'B' => 'job', 'C' => 'root', 'D' => 'root' }, - job_variables: { 'A' => 'job', 'B' => 'job' }, - root_variables_inheritance: true - ) - end - - context 'when inherit of defaults is disabled' do - let(:config) do - { - variables: { A: 'job', B: 'job' }, - inherit: { variables: false } - } - end - - it 'does return job and root variables' do - expect(entry.value).to include( - variables: { 'A' => 'job', 'B' => 'job' }, - job_variables: { 'A' => 'job', 'B' => 'job' }, - root_variables_inheritance: false - ) - end - end - - context 'when inherit of only specific variable is enabled' do - let(:config) do - { - variables: { A: 'job', B: 'job' }, - inherit: { variables: ['D'] } - } - end - - it 'does return job and root variables' do - expect(entry.value).to include( - variables: { 'A' => 'job', 'B' => 'job', 'D' => 'root' }, - job_variables: { 'A' => 'job', 'B' => 'job' }, - root_variables_inheritance: ['D'] - ) - end - end - end - end - context 'of default:tags' do using RSpec::Parameterized::TableSyntax @@ -493,7 +423,6 @@ RSpec.describe Gitlab::Ci::Config::Entry::Processable do name: :rspec, stage: 'test', only: { refs: %w[branches tags] }, - variables: {}, job_variables: {}, root_variables_inheritance: true ) diff --git a/spec/lib/gitlab/ci/config/entry/root_spec.rb b/spec/lib/gitlab/ci/config/entry/root_spec.rb index 31e3545e8d8..d862fbf5b78 100644 --- a/spec/lib/gitlab/ci/config/entry/root_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/root_spec.rb @@ -132,7 +132,6 @@ 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' }], - variables: { 'VAR' => 'root', 'VAR2' => 'val 2' }, job_variables: {}, root_variables_inheritance: true, ignore: false, @@ -148,7 +147,6 @@ 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' }], - variables: { 'VAR' => 'root', 'VAR2' => 'val 2' }, job_variables: {}, root_variables_inheritance: true, ignore: false, @@ -166,7 +164,6 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do services: [{ name: "postgres:9.1" }, { name: "mysql:5.5" }], cache: [{ key: "k", untracked: true, paths: ["public/"], policy: "pull-push", when: 'on_success' }], only: { refs: %w(branches tags) }, - variables: { 'VAR' => 'job', 'VAR2' => 'val 2' }, job_variables: { 'VAR' => 'job' }, root_variables_inheritance: true, after_script: [], @@ -214,7 +211,6 @@ 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' }], - variables: { 'VAR' => 'root' }, job_variables: {}, root_variables_inheritance: true, ignore: false, @@ -228,7 +224,6 @@ 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' }], - variables: { 'VAR' => 'job' }, job_variables: { 'VAR' => 'job' }, root_variables_inheritance: true, ignore: false, diff --git a/spec/lib/gitlab/ci/config/entry/rules_spec.rb b/spec/lib/gitlab/ci/config/entry/rules_spec.rb index 7d26365e7b3..91252378541 100644 --- a/spec/lib/gitlab/ci/config/entry/rules_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/rules_spec.rb @@ -17,6 +17,10 @@ RSpec.describe Gitlab::Ci::Config::Entry::Rules do describe '.new' do subject { entry } + before do + subject.compose! + end + context 'with a list of rule rule' do let(:config) do [{ if: '$THIS == "that"', when: 'never' }] @@ -24,14 +28,6 @@ RSpec.describe Gitlab::Ci::Config::Entry::Rules do it { is_expected.to be_a(described_class) } it { is_expected.to be_valid } - - context 'when composed' do - before do - subject.compose! - end - - it { is_expected.to be_valid } - end end context 'with a list of two rules' do @@ -42,21 +38,34 @@ RSpec.describe Gitlab::Ci::Config::Entry::Rules do ] end - it { is_expected.to be_a(described_class) } it { is_expected.to be_valid } + end - context 'when composed' do - before do - subject.compose! - end + context 'with a single rule object' do + let(:config) do + { if: '$SKIP', when: 'never' } + end - it { is_expected.to be_valid } + it { is_expected.not_to be_valid } + end + + context 'with nested rules' do + let(:config) do + [ + { if: '$THIS == "that"', when: 'always' }, + [{ if: '$SKIP', when: 'never' }] + ] end + + it { is_expected.to be_valid } end - context 'with a single rule object' do + context 'with rules nested more than one level' do let(:config) do - { if: '$SKIP', when: 'never' } + [ + { if: '$THIS == "that"', when: 'always' }, + [{ if: '$SKIP', when: 'never' }, [{ if: '$THIS == "other"', when: 'aways' }]] + ] end it { is_expected.not_to be_valid } @@ -90,7 +99,36 @@ RSpec.describe Gitlab::Ci::Config::Entry::Rules do { if: '$SKIP', when: 'never' } end - it { is_expected.to eq(config) } + it { is_expected.to eq([config]) } + end + + context 'with nested rules' do + let(:first_rule) { { if: '$THIS == "that"', when: 'always' } } + let(:second_rule) { { if: '$SKIP', when: 'never' } } + + let(:config) do + [ + first_rule, + [second_rule] + ] + end + + it { is_expected.to contain_exactly(first_rule, second_rule) } + end + + context 'with rules nested more than one level' do + let(:first_rule) { { if: '$THIS == "that"', when: 'always' } } + let(:second_rule) { { if: '$SKIP', when: 'never' } } + let(:third_rule) { { if: '$THIS == "other"', when: 'aways' } } + + let(:config) do + [ + first_rule, + [second_rule, [third_rule]] + ] + end + + it { is_expected.to contain_exactly(first_rule, second_rule, third_rule) } 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 88097f3f56a..a471997e43a 100644 --- a/spec/lib/gitlab/ci/config/external/mapper_spec.rb +++ b/spec/lib/gitlab/ci/config/external/mapper_spec.rb @@ -11,7 +11,7 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper do let(:local_file) { '/lib/gitlab/ci/templates/non-existent-file.yml' } let(:remote_url) { 'https://gitlab.com/gitlab-org/gitlab-foss/blob/1234/.gitlab-ci-1.yml' } let(:template_file) { 'Auto-DevOps.gitlab-ci.yml' } - let(:context_params) { { project: project, sha: '123456', user: user, variables: project.predefined_variables.to_runner_variables } } + let(:context_params) { { project: project, sha: '123456', user: user, variables: project.predefined_variables } } let(:context) { Gitlab::Ci::Config::External::Context.new(**context_params) } let(:file_content) do @@ -347,15 +347,51 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper do expect(subject.map(&:location)).to contain_exactly('myfolder/file1.yml', 'myfolder/file2.yml') end + end + + context "when 'include' has rules" do + let(:values) do + { include: [{ remote: remote_url }, + { local: local_file, rules: [{ if: "$CI_PROJECT_ID == '#{project_id}'" }] }], + image: 'ruby:2.7' } + end - context 'when the FF ci_wildcard_file_paths is disabled' do - before do - stub_feature_flags(ci_wildcard_file_paths: false) + context 'when the rules matches' do + let(:project_id) { project.id } + + it 'includes the file' do + expect(subject).to contain_exactly(an_instance_of(Gitlab::Ci::Config::External::File::Remote), + an_instance_of(Gitlab::Ci::Config::External::File::Local)) end - it 'cannot find any file returns an error message' do - expect(subject).to contain_exactly(an_instance_of(Gitlab::Ci::Config::External::File::Local)) - expect(subject[0].errors).to eq(['Local file `myfolder/*.yml` does not exist!']) + context 'when the FF ci_include_rules is disabled' do + before do + stub_feature_flags(ci_include_rules: false) + end + + it 'includes the file' do + expect(subject).to contain_exactly(an_instance_of(Gitlab::Ci::Config::External::File::Remote), + an_instance_of(Gitlab::Ci::Config::External::File::Local)) + end + end + end + + context 'when the rules does not match' do + let(:project_id) { non_existing_record_id } + + it 'does not include the file' do + expect(subject).to contain_exactly(an_instance_of(Gitlab::Ci::Config::External::File::Remote)) + end + + context 'when the FF ci_include_rules is disabled' do + before do + stub_feature_flags(ci_include_rules: false) + end + + it 'includes the file' do + expect(subject).to contain_exactly(an_instance_of(Gitlab::Ci::Config::External::File::Remote), + an_instance_of(Gitlab::Ci::Config::External::File::Local)) + end end end end diff --git a/spec/lib/gitlab/ci/config/external/rules_spec.rb b/spec/lib/gitlab/ci/config/external/rules_spec.rb new file mode 100644 index 00000000000..89ea13d710d --- /dev/null +++ b/spec/lib/gitlab/ci/config/external/rules_spec.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' + +RSpec.describe Gitlab::Ci::Config::External::Rules do + let(:rule_hashes) {} + + subject(:rules) { described_class.new(rule_hashes) } + + describe '#evaluate' do + let(:context) { double(variables: {}) } + + subject(:result) { rules.evaluate(context).pass? } + + context 'when there is no rule' do + it { is_expected.to eq(true) } + end + + context 'when there is a rule' do + let(:rule_hashes) { [{ if: '$MY_VAR == "hello"' }] } + + context 'when the rule matches' do + let(:context) { double(variables: { MY_VAR: 'hello' }) } + + it { is_expected.to eq(true) } + end + + context 'when the rule does not match' do + let(:context) { double(variables: { MY_VAR: 'invalid' }) } + + it { is_expected.to eq(false) } + end + end + end +end diff --git a/spec/lib/gitlab/ci/config/normalizer/matrix_strategy_spec.rb b/spec/lib/gitlab/ci/config/normalizer/matrix_strategy_spec.rb index e5f0341c5fe..a29471706cc 100644 --- a/spec/lib/gitlab/ci/config/normalizer/matrix_strategy_spec.rb +++ b/spec/lib/gitlab/ci/config/normalizer/matrix_strategy_spec.rb @@ -50,10 +50,6 @@ RSpec.describe Gitlab::Ci::Config::Normalizer::MatrixStrategy do name: 'test: [aws, app1]', instance: 1, parallel: { total: 4 }, - variables: { - 'PROVIDER' => 'aws', - 'STACK' => 'app1' - }, job_variables: { 'PROVIDER' => 'aws', 'STACK' => 'app1' @@ -63,10 +59,6 @@ RSpec.describe Gitlab::Ci::Config::Normalizer::MatrixStrategy do name: 'test: [aws, app2]', instance: 2, parallel: { total: 4 }, - variables: { - 'PROVIDER' => 'aws', - 'STACK' => 'app2' - }, job_variables: { 'PROVIDER' => 'aws', 'STACK' => 'app2' @@ -76,10 +68,6 @@ RSpec.describe Gitlab::Ci::Config::Normalizer::MatrixStrategy do name: 'test: [ovh, app]', instance: 3, parallel: { total: 4 }, - variables: { - 'PROVIDER' => 'ovh', - 'STACK' => 'app' - }, job_variables: { 'PROVIDER' => 'ovh', 'STACK' => 'app' @@ -89,10 +77,6 @@ RSpec.describe Gitlab::Ci::Config::Normalizer::MatrixStrategy do name: 'test: [gcp, app]', instance: 4, parallel: { total: 4 }, - variables: { - 'PROVIDER' => 'gcp', - 'STACK' => 'app' - }, job_variables: { 'PROVIDER' => 'gcp', 'STACK' => 'app' diff --git a/spec/lib/gitlab/ci/config/normalizer_spec.rb b/spec/lib/gitlab/ci/config/normalizer_spec.rb index 4c19657413c..354392eb42e 100644 --- a/spec/lib/gitlab/ci/config/normalizer_spec.rb +++ b/spec/lib/gitlab/ci/config/normalizer_spec.rb @@ -4,7 +4,7 @@ require 'fast_spec_helper' RSpec.describe Gitlab::Ci::Config::Normalizer do let(:job_name) { :rspec } - let(:job_config) { { script: 'rspec', parallel: parallel_config, name: 'rspec', variables: variables_config } } + let(:job_config) { { script: 'rspec', parallel: parallel_config, name: 'rspec', job_variables: variables_config } } let(:config) { { job_name => job_config } } describe '.normalize_jobs' do @@ -202,21 +202,21 @@ RSpec.describe Gitlab::Ci::Config::Normalizer do it 'sets job variables', :aggregate_failures do expect(subject.values[0]).to match( - a_hash_including(variables: { VAR_1: 'A', VAR_2: 'B', USER_VARIABLE: 'user value' }) + a_hash_including(job_variables: { VAR_1: 'A', VAR_2: 'B', USER_VARIABLE: 'user value' }) ) expect(subject.values[1]).to match( - a_hash_including(variables: { VAR_1: 'A', VAR_2: 'C', USER_VARIABLE: 'user value' }) + a_hash_including(job_variables: { VAR_1: 'A', VAR_2: 'C', USER_VARIABLE: 'user value' }) ) end it 'parallelizes jobs with original config' do configs = subject.values.map do |config| - config.except(:name, :instance, :variables) + config.except(:name, :instance, :job_variables) end original_config = config[job_name] - .except(:name, :variables) + .except(:name, :job_variables) .deep_merge(parallel: { total: 2 }) expect(configs).to all(match(a_hash_including(original_config))) diff --git a/spec/lib/gitlab/ci/config_spec.rb b/spec/lib/gitlab/ci/config_spec.rb index 45ce4cac6c4..3ec4519748f 100644 --- a/spec/lib/gitlab/ci/config_spec.rb +++ b/spec/lib/gitlab/ci/config_spec.rb @@ -286,7 +286,9 @@ RSpec.describe Gitlab::Ci::Config do end context "when using 'include' directive" do - let(:project) { create(:project, :repository) } + let(:group) { create(:group) } + let(:project) { create(:project, :repository, group: group) } + let(:main_project) { create(:project, :repository, :public, group: group) } let(:remote_location) { 'https://gitlab.com/gitlab-org/gitlab-foss/blob/1234/.gitlab-ci-1.yml' } let(:local_location) { 'spec/fixtures/gitlab/ci/external_files/.gitlab-ci-template-1.yml' } @@ -317,7 +319,9 @@ RSpec.describe Gitlab::Ci::Config do include: - #{local_location} - #{remote_location} - + - project: '$MAIN_PROJECT' + ref: '$REF' + file: '$FILENAME' image: ruby:2.7 HEREDOC end @@ -331,6 +335,26 @@ RSpec.describe Gitlab::Ci::Config do allow(project.repository) .to receive(:blob_data_at).and_return(local_file_content) + + main_project.repository.create_file( + main_project.creator, + '.gitlab-ci.yml', + local_file_content, + message: 'Add README.md', + branch_name: 'master' + ) + + main_project.repository.create_file( + main_project.creator, + '.another-ci-file.yml', + local_file_content, + message: 'Add README.md', + branch_name: 'master' + ) + + create(:ci_variable, project: project, key: "REF", value: "HEAD") + create(:ci_group_variable, group: group, key: "FILENAME", value: ".gitlab-ci.yml") + create(:ci_instance_variable, key: 'MAIN_PROJECT', value: main_project.full_path) end context "when gitlab_ci_yml has valid 'include' defined" do @@ -344,6 +368,38 @@ RSpec.describe Gitlab::Ci::Config do expect(config.to_hash).to eq(composed_hash) end + + context 'handling variables' do + it 'contains all project variables' do + ref = config.context.variables.find { |v| v[:key] == 'REF' } + + expect(ref[:value]).to eq("HEAD") + end + + it 'contains all group variables' do + filename = config.context.variables.find { |v| v[:key] == 'FILENAME' } + + expect(filename[:value]).to eq(".gitlab-ci.yml") + end + + it 'contains all instance variables' do + project = config.context.variables.find { |v| v[:key] == 'MAIN_PROJECT' } + + expect(project[:value]).to eq(main_project.full_path) + end + + context 'overriding a group variable at project level' do + before do + create(:ci_variable, project: project, key: "FILENAME", value: ".another-ci-file.yml") + end + + it 'successfully overrides' do + filename = config.context.variables.to_hash[:FILENAME] + + expect(filename).to eq('.another-ci-file.yml') + end + end + end end context "when gitlab_ci.yml has invalid 'include' defined" do @@ -667,5 +723,33 @@ RSpec.describe Gitlab::Ci::Config do expect(config.to_hash).to eq(composed_hash) end end + + context "when an 'include' has rules" do + let(:gitlab_ci_yml) do + <<~HEREDOC + include: + - local: #{local_location} + rules: + - if: $CI_PROJECT_ID == "#{project_id}" + image: ruby:2.7 + HEREDOC + end + + context 'when the rules condition is satisfied' do + let(:project_id) { project.id } + + it 'includes the file' do + expect(config.to_hash).to include(local_location_hash) + end + end + + context 'when the rules condition is satisfied' do + let(:project_id) { non_existing_record_id } + + it 'does not include the file' do + expect(config.to_hash).not_to include(local_location_hash) + end + end + end end end diff --git a/spec/lib/gitlab/ci/lint_spec.rb b/spec/lib/gitlab/ci/lint_spec.rb index 77f6608eb85..1e433d7854a 100644 --- a/spec/lib/gitlab/ci/lint_spec.rb +++ b/spec/lib/gitlab/ci/lint_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' RSpec.describe Gitlab::Ci::Lint do - let(:project) { create(:project, :repository) } + let_it_be(:project) { create(:project, :repository) } let_it_be(:user) { create(:user) } let(:lint) { described_class.new(project: project, current_user: user) } @@ -89,6 +89,15 @@ RSpec.describe Gitlab::Ci::Lint do ) end + after do + project.repository.delete_file( + project.creator, + 'another-gitlab-ci.yml', + message: 'Remove another-gitlab-ci.yml', + branch_name: 'master' + ) + end + it 'sets merged_config' do root_config = YAML.safe_load(content, [Symbol]) included_config = YAML.safe_load(included_content, [Symbol]) diff --git a/spec/lib/gitlab/ci/parsers/security/common_spec.rb b/spec/lib/gitlab/ci/parsers/security/common_spec.rb new file mode 100644 index 00000000000..c6387bf615b --- /dev/null +++ b/spec/lib/gitlab/ci/parsers/security/common_spec.rb @@ -0,0 +1,350 @@ +# frozen_string_literal: true + +# TODO remove duplication from spec/lib/gitlab/ci/parsers/security/common_spec.rb and spec/lib/gitlab/ci/parsers/security/common_spec.rb +# See https://gitlab.com/gitlab-org/gitlab/-/issues/336589 +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Parsers::Security::Common do + describe '#parse!' do + where(vulnerability_finding_signatures_enabled: [true, false]) + with_them do + let_it_be(:pipeline) { create(:ci_pipeline) } + + let(:artifact) { build(:ci_job_artifact, :common_security_report) } + let(:report) { Gitlab::Ci::Reports::Security::Report.new(artifact.file_type, pipeline, 2.weeks.ago) } + # The path 'yarn.lock' was initially used by DependencyScanning, it is okay for SAST locations to use it, but this could be made better + let(:location) { ::Gitlab::Ci::Reports::Security::Locations::Sast.new(file_path: 'yarn.lock', start_line: 1, end_line: 1) } + let(:tracking_data) { nil } + + before do + allow_next_instance_of(described_class) do |parser| + allow(parser).to receive(:create_location).and_return(location) + allow(parser).to receive(:tracking_data).and_return(tracking_data) + end + + artifact.each_blob { |blob| described_class.parse!(blob, report, vulnerability_finding_signatures_enabled) } + end + + describe 'schema validation' do + let(:validator_class) { Gitlab::Ci::Parsers::Security::Validators::SchemaValidator } + let(:parser) { described_class.new('{}', report, vulnerability_finding_signatures_enabled, validate: validate) } + + subject(:parse_report) { parser.parse! } + + before do + allow(validator_class).to receive(:new).and_call_original + end + + context 'when the validate flag is set as `false`' do + let(:validate) { false } + + it 'does not run the validation logic' do + parse_report + + expect(validator_class).not_to have_received(:new) + end + end + + context 'when the validate flag is set as `true`' do + let(:validate) { true } + let(:valid?) { false } + + before do + allow_next_instance_of(validator_class) do |instance| + allow(instance).to receive(:valid?).and_return(valid?) + allow(instance).to receive(:errors).and_return(['foo']) + end + + allow(parser).to receive_messages(create_scanner: true, create_scan: true) + end + + it 'instantiates the validator with correct params' do + parse_report + + expect(validator_class).to have_received(:new).with(report.type, {}) + end + + context 'when the report data is not valid according to the schema' do + it 'adds errors to the report' do + expect { parse_report }.to change { report.errors }.from([]).to([{ message: 'foo', type: 'Schema' }]) + end + + it 'does not try to create report entities' do + parse_report + + expect(parser).not_to have_received(:create_scanner) + expect(parser).not_to have_received(:create_scan) + end + end + + context 'when the report data is valid according to the schema' do + let(:valid?) { true } + + it 'does not add errors to the report' do + expect { parse_report }.not_to change { report.errors }.from([]) + end + + it 'keeps the execution flow as normal' do + parse_report + + expect(parser).to have_received(:create_scanner) + expect(parser).to have_received(:create_scan) + end + end + end + end + + describe 'parsing finding.name' do + let(:artifact) { build(:ci_job_artifact, :common_security_report_with_blank_names) } + + context 'when message is provided' do + it 'sets message from the report as a finding name' do + finding = report.findings.find { |x| x.compare_key == 'CVE-1020' } + expected_name = Gitlab::Json.parse(finding.raw_metadata)['message'] + + expect(finding.name).to eq(expected_name) + end + end + + context 'when message is not provided' do + context 'and name is provided' do + it 'sets name from the report as a name' do + finding = report.findings.find { |x| x.compare_key == 'CVE-1030' } + expected_name = Gitlab::Json.parse(finding.raw_metadata)['name'] + + expect(finding.name).to eq(expected_name) + end + end + + context 'and name is not provided' do + context 'when CVE identifier exists' do + it 'combines identifier with location to create name' do + finding = report.findings.find { |x| x.compare_key == 'CVE-2017-11429' } + expect(finding.name).to eq("CVE-2017-11429 in yarn.lock") + end + end + + context 'when CWE identifier exists' do + it 'combines identifier with location to create name' do + finding = report.findings.find { |x| x.compare_key == 'CWE-2017-11429' } + expect(finding.name).to eq("CWE-2017-11429 in yarn.lock") + end + end + + context 'when neither CVE nor CWE identifier exist' do + it 'combines identifier with location to create name' do + finding = report.findings.find { |x| x.compare_key == 'OTHER-2017-11429' } + expect(finding.name).to eq("other-2017-11429 in yarn.lock") + end + end + end + end + end + + describe 'parsing finding.details' do + context 'when details are provided' do + it 'sets details from the report' do + finding = report.findings.find { |x| x.compare_key == 'CVE-1020' } + expected_details = Gitlab::Json.parse(finding.raw_metadata)['details'] + + expect(finding.details).to eq(expected_details) + end + end + + context 'when details are not provided' do + it 'sets empty hash' do + finding = report.findings.find { |x| x.compare_key == 'CVE-1030' } + expect(finding.details).to eq({}) + end + end + end + + describe 'top-level scanner' do + it 'is the primary scanner' do + expect(report.primary_scanner.external_id).to eq('gemnasium') + expect(report.primary_scanner.name).to eq('Gemnasium') + expect(report.primary_scanner.vendor).to eq('GitLab') + expect(report.primary_scanner.version).to eq('2.18.0') + end + + it 'returns nil report has no scanner' do + empty_report = Gitlab::Ci::Reports::Security::Report.new(artifact.file_type, pipeline, 2.weeks.ago) + described_class.parse!({}.to_json, empty_report) + + expect(empty_report.primary_scanner).to be_nil + end + end + + describe 'parsing scanners' do + subject(:scanner) { report.findings.first.scanner } + + context 'when vendor is not missing in scanner' do + it 'returns scanner with parsed vendor value' do + expect(scanner.vendor).to eq('GitLab') + end + end + end + + describe 'parsing scan' do + it 'returns scan object for each finding' do + scans = report.findings.map(&:scan) + + expect(scans.map(&:status).all?('success')).to be(true) + expect(scans.map(&:start_time).all?('placeholder-value')).to be(true) + expect(scans.map(&:end_time).all?('placeholder-value')).to be(true) + expect(scans.size).to eq(3) + expect(scans.first).to be_a(::Gitlab::Ci::Reports::Security::Scan) + end + + it 'returns nil when scan is not a hash' do + empty_report = Gitlab::Ci::Reports::Security::Report.new(artifact.file_type, pipeline, 2.weeks.ago) + described_class.parse!({}.to_json, empty_report) + + expect(empty_report.scan).to be(nil) + end + end + + describe 'parsing schema version' do + it 'parses the version' do + expect(report.version).to eq('14.0.2') + end + + it 'returns nil when there is no version' do + empty_report = Gitlab::Ci::Reports::Security::Report.new(artifact.file_type, pipeline, 2.weeks.ago) + described_class.parse!({}.to_json, empty_report) + + expect(empty_report.version).to be_nil + end + end + + describe 'parsing analyzer' do + it 'associates analyzer with report' do + expect(report.analyzer.id).to eq('common-analyzer') + expect(report.analyzer.name).to eq('Common Analyzer') + expect(report.analyzer.version).to eq('2.0.1') + expect(report.analyzer.vendor).to eq('Common') + end + + it 'returns nil when analyzer data is not available' do + empty_report = Gitlab::Ci::Reports::Security::Report.new(artifact.file_type, pipeline, 2.weeks.ago) + described_class.parse!({}.to_json, empty_report) + + expect(empty_report.analyzer).to be_nil + end + end + + describe 'parsing links' do + it 'returns links object for each finding', :aggregate_failures do + links = report.findings.flat_map(&:links) + + expect(links.map(&:url)).to match_array(['https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-1020', 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-1030']) + expect(links.map(&:name)).to match_array([nil, 'CVE-1030']) + expect(links.size).to eq(2) + expect(links.first).to be_a(::Gitlab::Ci::Reports::Security::Link) + end + end + + describe 'setting the uuid' do + let(:finding_uuids) { report.findings.map(&:uuid) } + let(:uuid_1) do + Security::VulnerabilityUUID.generate( + report_type: "sast", + primary_identifier_fingerprint: report.findings[0].identifiers.first.fingerprint, + location_fingerprint: location.fingerprint, + project_id: pipeline.project_id + ) + end + + let(:uuid_2) do + Security::VulnerabilityUUID.generate( + report_type: "sast", + primary_identifier_fingerprint: report.findings[1].identifiers.first.fingerprint, + location_fingerprint: location.fingerprint, + project_id: pipeline.project_id + ) + end + + let(:expected_uuids) { [uuid_1, uuid_2, nil] } + + it 'sets the UUIDv5 for findings', :aggregate_failures do + allow_next_instance_of(Gitlab::Ci::Reports::Security::Report) do |report| + allow(report).to receive(:type).and_return('sast') + + expect(finding_uuids).to match_array(expected_uuids) + end + end + end + + describe 'parsing tracking' do + let(:tracking_data) do + { + 'type' => 'source', + 'items' => [ + 'signatures' => [ + { 'algorithm' => 'hash', 'value' => 'hash_value' }, + { 'algorithm' => 'location', 'value' => 'location_value' }, + { 'algorithm' => 'scope_offset', 'value' => 'scope_offset_value' } + ] + ] + } + end + + context 'with valid tracking information' do + it 'creates signatures for each algorithm' do + finding = report.findings.first + expect(finding.signatures.size).to eq(3) + expect(finding.signatures.map(&:algorithm_type).to_set).to eq(Set['hash', 'location', 'scope_offset']) + end + end + + context 'with invalid tracking information' do + let(:tracking_data) do + { + 'type' => 'source', + 'items' => [ + 'signatures' => [ + { 'algorithm' => 'hash', 'value' => 'hash_value' }, + { 'algorithm' => 'location', 'value' => 'location_value' }, + { 'algorithm' => 'INVALID', 'value' => 'scope_offset_value' } + ] + ] + } + end + + it 'ignores invalid algorithm types' do + finding = report.findings.first + expect(finding.signatures.size).to eq(2) + expect(finding.signatures.map(&:algorithm_type).to_set).to eq(Set['hash', 'location']) + end + end + + context 'with valid tracking information' do + it 'creates signatures for each signature algorithm' do + finding = report.findings.first + expect(finding.signatures.size).to eq(3) + expect(finding.signatures.map(&:algorithm_type)).to eq(%w[hash location scope_offset]) + + signatures = finding.signatures.index_by(&:algorithm_type) + expected_values = tracking_data['items'][0]['signatures'].index_by { |x| x['algorithm'] } + expect(signatures['hash'].signature_value).to eq(expected_values['hash']['value']) + expect(signatures['location'].signature_value).to eq(expected_values['location']['value']) + expect(signatures['scope_offset'].signature_value).to eq(expected_values['scope_offset']['value']) + end + + it 'sets the uuid according to the higest priority signature' do + finding = report.findings.first + highest_signature = finding.signatures.max_by(&:priority) + + identifiers = if vulnerability_finding_signatures_enabled + "#{finding.report_type}-#{finding.primary_identifier.fingerprint}-#{highest_signature.signature_hex}-#{report.project_id}" + else + "#{finding.report_type}-#{finding.primary_identifier.fingerprint}-#{finding.location.fingerprint}-#{report.project_id}" + end + + expect(finding.uuid).to eq(Gitlab::UUID.v5(identifiers)) + end + end + end + end + end +end diff --git a/spec/lib/gitlab/ci/parsers/security/sast_spec.rb b/spec/lib/gitlab/ci/parsers/security/sast_spec.rb new file mode 100644 index 00000000000..4bc48f6611a --- /dev/null +++ b/spec/lib/gitlab/ci/parsers/security/sast_spec.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Parsers::Security::Sast do + using RSpec::Parameterized::TableSyntax + + describe '#parse!' do + let_it_be(:pipeline) { create(:ci_pipeline) } + + let(:created_at) { 2.weeks.ago } + + context "when parsing valid reports" do + where(:report_format, :report_version, :scanner_length, :finding_length, :identifier_length, :file_path, :line) do + :sast | '14.0.0' | 1 | 5 | 6 | 'groovy/src/main/java/com/gitlab/security_products/tests/App.groovy' | 47 + :sast_deprecated | '1.2' | 3 | 33 | 17 | 'python/hardcoded/hardcoded-tmp.py' | 1 + end + + with_them do + let(:report) { Gitlab::Ci::Reports::Security::Report.new(artifact.file_type, pipeline, created_at) } + let(:artifact) { create(:ci_job_artifact, report_format) } + + before do + artifact.each_blob { |blob| described_class.parse!(blob, report) } + end + + it "parses all identifiers and findings" do + expect(report.findings.length).to eq(finding_length) + expect(report.identifiers.length).to eq(identifier_length) + expect(report.scanners.length).to eq(scanner_length) + end + + it 'generates expected location' do + location = report.findings.first.location + + expect(location).to be_a(::Gitlab::Ci::Reports::Security::Locations::Sast) + expect(location).to have_attributes( + file_path: file_path, + end_line: line, + start_line: line + ) + end + + it "generates expected metadata_version" do + expect(report.findings.first.metadata_version).to eq(report_version) + end + end + end + + context "when parsing an empty report" do + let(:report) { Gitlab::Ci::Reports::Security::Report.new('sast', pipeline, created_at) } + let(:blob) { Gitlab::Json.generate({}) } + + it { expect(described_class.parse!(blob, report)).to be_empty } + end + end +end diff --git a/spec/lib/gitlab/ci/parsers/security/secret_detection_spec.rb b/spec/lib/gitlab/ci/parsers/security/secret_detection_spec.rb new file mode 100644 index 00000000000..1d361e16aad --- /dev/null +++ b/spec/lib/gitlab/ci/parsers/security/secret_detection_spec.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Parsers::Security::SecretDetection do + describe '#parse!' do + let_it_be(:pipeline) { create(:ci_pipeline) } + + let(:created_at) { 2.weeks.ago } + + context "when parsing valid reports" do + where(report_format: %i(secret_detection)) + + with_them do + let(:report) { Gitlab::Ci::Reports::Security::Report.new(artifact.file_type, pipeline, created_at) } + let(:artifact) { create(:ci_job_artifact, report_format) } + + before do + artifact.each_blob { |blob| described_class.parse!(blob, report) } + end + + it "parses all identifiers and findings" do + expect(report.findings.length).to eq(1) + expect(report.identifiers.length).to eq(1) + expect(report.scanners.length).to eq(1) + end + + it 'generates expected location' do + location = report.findings.first.location + + expect(location).to be_a(::Gitlab::Ci::Reports::Security::Locations::SecretDetection) + expect(location).to have_attributes( + file_path: 'aws-key.py', + start_line: nil, + end_line: nil, + class_name: nil, + method_name: nil + ) + end + + it "generates expected metadata_version" do + expect(report.findings.first.metadata_version).to eq('3.0') + end + end + end + + context "when parsing an empty report" do + let(:report) { Gitlab::Ci::Reports::Security::Report.new('secret_detection', pipeline, created_at) } + let(:blob) { Gitlab::Json.generate({}) } + + it { expect(described_class.parse!(blob, report)).to be_empty } + 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 new file mode 100644 index 00000000000..f434ffd12bf --- /dev/null +++ b/spec/lib/gitlab/ci/parsers/security/validators/schema_validator_spec.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator do + using RSpec::Parameterized::TableSyntax + + where(:report_type, :expected_errors, :valid_data) do + :sast | ['root is missing required keys: vulnerabilities'] | { 'version' => '10.0.0', 'vulnerabilities' => [] } + :secret_detection | ['root is missing required keys: vulnerabilities'] | { 'version' => '10.0.0', 'vulnerabilities' => [] } + end + + with_them do + let(:validator) { described_class.new(report_type, report_data) } + + describe '#valid?' do + subject { validator.valid? } + + context 'when given data is invalid according to the schema' do + let(:report_data) { {} } + + it { is_expected.to be_falsey } + end + + context 'when given data is valid according to the schema' do + let(:report_data) { valid_data } + + it { is_expected.to be_truthy } + end + end + + describe '#errors' do + let(:report_data) { { 'version' => '10.0.0' } } + + subject { validator.errors } + + it { is_expected.to eq(expected_errors) } + end + end +end diff --git a/spec/lib/gitlab/ci/pipeline/chain/command_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/command_spec.rb index 2e73043e309..c22a0e23794 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/command_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/command_spec.rb @@ -295,31 +295,6 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Command do end end - describe '#dangling_build?' do - let(:project) { create(:project, :repository) } - let(:command) { described_class.new(project: project, source: source) } - - subject { command.dangling_build? } - - context 'when source is :webide' do - let(:source) { :webide } - - it { is_expected.to eq(true) } - end - - context 'when source is :ondemand_dast_scan' do - let(:source) { :ondemand_dast_scan } - - it { is_expected.to eq(true) } - end - - context 'when source something else' do - let(:source) { :web } - - it { is_expected.to eq(false) } - end - end - describe '#creates_child_pipeline?' do let(:command) { described_class.new(bridge: bridge) } diff --git a/spec/lib/gitlab/ci/pipeline/chain/limit/deployments_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/limit/deployments_spec.rb index 499dc3554a3..1aa104310af 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/limit/deployments_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/limit/deployments_spec.rb @@ -85,7 +85,7 @@ RSpec.describe ::Gitlab::Ci::Pipeline::Chain::Limit::Deployments do end it 'logs the error' do - expect(Gitlab::ErrorTracking).to receive(:track_exception).with( + expect(Gitlab::ErrorTracking).to receive(:log_exception).with( instance_of(Gitlab::Ci::Limit::LimitExceededError), project_id: project.id, plan: namespace.actual_plan_name ) diff --git a/spec/lib/gitlab/ci/pipeline/chain/sequence_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/sequence_spec.rb index cc4aaffb0a4..83d47ae6819 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/sequence_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/sequence_spec.rb @@ -7,7 +7,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Sequence do let_it_be(:user) { create(:user) } let(:pipeline) { build_stubbed(:ci_pipeline) } - let(:command) { Gitlab::Ci::Pipeline::Chain::Command.new } + let(:command) { Gitlab::Ci::Pipeline::Chain::Command.new(project: project) } let(:first_step) { spy('first step') } let(:second_step) { spy('second step') } let(:sequence) { [first_step, second_step] } @@ -71,5 +71,20 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Sequence do expect(histogram).to have_received(:observe) .with({ source: 'push' }, 0) end + + it 'records active jobs by pipeline plan in a histogram' do + allow(command.metrics) + .to receive(:active_jobs_histogram) + .and_return(histogram) + + pipeline = create(:ci_pipeline, project: project, status: :running) + create(:ci_build, :finished, project: project, pipeline: pipeline) + create(:ci_build, :failed, project: project, pipeline: pipeline) + create(:ci_build, :running, project: project, pipeline: pipeline) + subject.build! + + expect(histogram).to have_received(:observe) + .with(hash_including(plan: project.actual_plan_name), 3) + end end end diff --git a/spec/lib/gitlab/ci/reports/security/aggregated_report_spec.rb b/spec/lib/gitlab/ci/reports/security/aggregated_report_spec.rb new file mode 100644 index 00000000000..c56177a6453 --- /dev/null +++ b/spec/lib/gitlab/ci/reports/security/aggregated_report_spec.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Reports::Security::AggregatedReport do + subject { described_class.new(reports, findings) } + + let(:reports) { build_list(:ci_reports_security_report, 1) } + let(:findings) { build_list(:ci_reports_security_finding, 1) } + + describe '#created_at' do + context 'no reports' do + let(:reports) { [] } + + it 'has no created date' do + expect(subject.created_at).to be_nil + end + end + + context 'report with no created date' do + let(:reports) { build_list(:ci_reports_security_report, 1, created_at: nil) } + + it 'has no created date' do + expect(subject.created_at).to be_nil + end + end + + context 'has reports' do + let(:a_long_time_ago) { 2.months.ago } + let(:a_while_ago) { 2.weeks.ago } + let(:yesterday) { 1.day.ago } + + let(:reports) do + [build(:ci_reports_security_report, created_at: a_while_ago), + build(:ci_reports_security_report, created_at: a_long_time_ago), + build(:ci_reports_security_report, created_at: nil), + build(:ci_reports_security_report, created_at: yesterday)] + end + + it 'has oldest created date' do + expect(subject.created_at).to eq(a_long_time_ago) + end + end + end +end diff --git a/spec/lib/gitlab/ci/reports/security/finding_key_spec.rb b/spec/lib/gitlab/ci/reports/security/finding_key_spec.rb new file mode 100644 index 00000000000..784c1183320 --- /dev/null +++ b/spec/lib/gitlab/ci/reports/security/finding_key_spec.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Reports::Security::FindingKey do + using RSpec::Parameterized::TableSyntax + + describe '#==' do + where(:location_fp_1, :location_fp_2, :identifier_fp_1, :identifier_fp_2, :equals?) do + nil | 'different location fp' | 'identifier fp' | 'different identifier fp' | false + 'location fp' | nil | 'identifier fp' | 'different identifier fp' | false + 'location fp' | 'different location fp' | nil | 'different identifier fp' | false + 'location fp' | 'different location fp' | 'identifier fp' | nil | false + nil | nil | 'identifier fp' | 'identifier fp' | false + 'location fp' | 'location fp' | nil | nil | false + nil | nil | nil | nil | false + 'location fp' | 'different location fp' | 'identifier fp' | 'different identifier fp' | false + 'location fp' | 'different location fp' | 'identifier fp' | 'identifier fp' | false + 'location fp' | 'location fp' | 'identifier fp' | 'different identifier fp' | false + 'location fp' | 'location fp' | 'identifier fp' | 'identifier fp' | true + end + + with_them do + let(:finding_key_1) do + build(:ci_reports_security_finding_key, + location_fingerprint: location_fp_1, + identifier_fingerprint: identifier_fp_1) + end + + let(:finding_key_2) do + build(:ci_reports_security_finding_key, + location_fingerprint: location_fp_2, + identifier_fingerprint: identifier_fp_2) + end + + subject { finding_key_1 == finding_key_2 } + + it { is_expected.to be(equals?) } + end + end +end diff --git a/spec/lib/gitlab/ci/reports/security/finding_signature_spec.rb b/spec/lib/gitlab/ci/reports/security/finding_signature_spec.rb new file mode 100644 index 00000000000..23e6b40a039 --- /dev/null +++ b/spec/lib/gitlab/ci/reports/security/finding_signature_spec.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Reports::Security::FindingSignature do + subject { described_class.new(params.with_indifferent_access) } + + let(:params) do + { + algorithm_type: 'hash', + signature_value: 'SIGNATURE' + } + end + + describe '#initialize' do + context 'when a supported algorithm type is given' do + it 'allows itself to be created' do + expect(subject.algorithm_type).to eq(params[:algorithm_type]) + expect(subject.signature_value).to eq(params[:signature_value]) + end + + describe '#valid?' do + it 'returns true' do + expect(subject.valid?).to eq(true) + end + end + end + end + + describe '#valid?' do + context 'when supported algorithm_type is given' do + it 'is valid' do + expect(subject.valid?).to eq(true) + end + end + + context 'when an unsupported algorithm_type is given' do + let(:params) do + { + algorithm_type: 'INVALID', + signature_value: 'SIGNATURE' + } + end + + it 'is not valid' do + expect(subject.valid?).to eq(false) + end + end + end + + describe '#to_hash' do + it 'returns a hash representation of the signature' do + expect(subject.to_hash).to eq( + algorithm_type: params[:algorithm_type], + signature_sha: Digest::SHA1.digest(params[:signature_value]) + ) + end + end +end diff --git a/spec/lib/gitlab/ci/reports/security/locations/sast_spec.rb b/spec/lib/gitlab/ci/reports/security/locations/sast_spec.rb new file mode 100644 index 00000000000..effa7a60400 --- /dev/null +++ b/spec/lib/gitlab/ci/reports/security/locations/sast_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Reports::Security::Locations::Sast do + let(:params) do + { + file_path: 'src/main/App.java', + start_line: 29, + end_line: 31, + class_name: 'com.gitlab.security_products.tests.App', + method_name: 'insecureCypher' + } + end + + let(:mandatory_params) { %i[file_path start_line] } + let(:expected_fingerprint) { Digest::SHA1.hexdigest('src/main/App.java:29:31') } + let(:expected_fingerprint_path) { 'App.java' } + + it_behaves_like 'vulnerability location' +end diff --git a/spec/lib/gitlab/ci/reports/security/locations/secret_detection_spec.rb b/spec/lib/gitlab/ci/reports/security/locations/secret_detection_spec.rb new file mode 100644 index 00000000000..3b84a548713 --- /dev/null +++ b/spec/lib/gitlab/ci/reports/security/locations/secret_detection_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Reports::Security::Locations::SecretDetection do + let(:params) do + { + file_path: 'src/main/App.java', + start_line: 29, + end_line: 31, + class_name: 'com.gitlab.security_products.tests.App', + method_name: 'insecureCypher' + } + end + + let(:mandatory_params) { %i[file_path start_line] } + let(:expected_fingerprint) { Digest::SHA1.hexdigest('src/main/App.java:29:31') } + let(:expected_fingerprint_path) { 'App.java' } + + it_behaves_like 'vulnerability location' +end diff --git a/spec/lib/gitlab/ci/reports/security/report_spec.rb b/spec/lib/gitlab/ci/reports/security/report_spec.rb new file mode 100644 index 00000000000..5a85c3f19fc --- /dev/null +++ b/spec/lib/gitlab/ci/reports/security/report_spec.rb @@ -0,0 +1,224 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Reports::Security::Report do + let_it_be(:pipeline) { create(:ci_pipeline) } + + let(:created_at) { 2.weeks.ago } + + subject(:report) { described_class.new('sast', pipeline, created_at) } + + it { expect(report.type).to eq('sast') } + it { is_expected.to delegate_method(:project_id).to(:pipeline) } + + describe '#add_scanner' do + let(:scanner) { create(:ci_reports_security_scanner, external_id: 'find_sec_bugs') } + + subject { report.add_scanner(scanner) } + + it 'stores given scanner params in the map' do + subject + + expect(report.scanners).to eq({ 'find_sec_bugs' => scanner }) + end + + it 'returns the added scanner' do + expect(subject).to eq(scanner) + end + end + + describe '#add_identifier' do + let(:identifier) { create(:ci_reports_security_identifier) } + + subject { report.add_identifier(identifier) } + + it 'stores given identifier params in the map' do + subject + + expect(report.identifiers).to eq({ identifier.fingerprint => identifier }) + end + + it 'returns the added identifier' do + expect(subject).to eq(identifier) + end + end + + describe '#add_finding' do + let(:finding) { create(:ci_reports_security_finding) } + + it 'enriches given finding and stores it in the collection' do + report.add_finding(finding) + + expect(report.findings).to eq([finding]) + end + end + + describe '#clone_as_blank' do + let(:report) do + create( + :ci_reports_security_report, + findings: [create(:ci_reports_security_finding)], + scanners: [create(:ci_reports_security_scanner)], + identifiers: [create(:ci_reports_security_identifier)] + ) + end + + it 'creates a blank report with copied type and pipeline' do + clone = report.clone_as_blank + + expect(clone.type).to eq(report.type) + expect(clone.pipeline).to eq(report.pipeline) + expect(clone.created_at).to eq(report.created_at) + expect(clone.findings).to eq([]) + expect(clone.scanners).to eq({}) + expect(clone.identifiers).to eq({}) + end + end + + describe '#replace_with!' do + let(:report) do + create( + :ci_reports_security_report, + findings: [create(:ci_reports_security_finding)], + scanners: [create(:ci_reports_security_scanner)], + identifiers: [create(:ci_reports_security_identifier)] + ) + end + + let(:other_report) do + create( + :ci_reports_security_report, + findings: [create(:ci_reports_security_finding, compare_key: 'other_finding')], + scanners: [create(:ci_reports_security_scanner, external_id: 'other_scanner', name: 'Other Scanner')], + identifiers: [create(:ci_reports_security_identifier, external_id: 'other_id', name: 'other_scanner')] + ) + end + + before do + report.replace_with!(other_report) + end + + it 'replaces report contents with other reports contents' do + expect(report.findings).to eq(other_report.findings) + expect(report.scanners).to eq(other_report.scanners) + expect(report.identifiers).to eq(other_report.identifiers) + end + end + + describe '#merge!' do + let(:merged_report) { double('Report') } + + before do + merge_reports_service = double('MergeReportsService') + + allow(::Security::MergeReportsService).to receive(:new).and_return(merge_reports_service) + allow(merge_reports_service).to receive(:execute).and_return(merged_report) + allow(report).to receive(:replace_with!) + end + + subject { report.merge!(described_class.new('sast', pipeline, created_at)) } + + it 'invokes the merge with other report and then replaces this report contents by merge result' do + subject + + expect(report).to have_received(:replace_with!).with(merged_report) + end + end + + describe '#primary_scanner' do + let(:scanner_1) { create(:ci_reports_security_scanner, external_id: 'external_id_1') } + let(:scanner_2) { create(:ci_reports_security_scanner, external_id: 'external_id_2') } + + subject { report.primary_scanner } + + before do + report.add_scanner(scanner_1) + report.add_scanner(scanner_2) + end + + it { is_expected.to eq(scanner_1) } + end + + describe '#add_error' do + context 'when the message is not given' do + it 'adds a new error to report with the generic error message' do + expect { report.add_error('foo') }.to change { report.errors } + .from([]) + .to([{ type: 'foo', message: 'An unexpected error happened!' }]) + end + end + + context 'when the message is given' do + it 'adds a new error to report' do + expect { report.add_error('foo', 'bar') }.to change { report.errors } + .from([]) + .to([{ type: 'foo', message: 'bar' }]) + end + end + end + + describe 'errored?' do + subject { report.errored? } + + context 'when the report does not have any errors' do + it { is_expected.to be_falsey } + end + + context 'when the report has errors' do + before do + report.add_error('foo', 'bar') + end + + it { is_expected.to be_truthy } + end + end + + describe '#primary_scanner_order_to' do + let(:scanner_1) { build(:ci_reports_security_scanner) } + let(:scanner_2) { build(:ci_reports_security_scanner) } + let(:report_1) { described_class.new('sast', pipeline, created_at) } + let(:report_2) { described_class.new('sast', pipeline, created_at) } + + subject(:compare_based_on_primary_scanners) { report_1.primary_scanner_order_to(report_2) } + + context 'when the primary scanner of the receiver is nil' do + context 'when the primary scanner of the other is nil' do + it { is_expected.to be(1) } + end + + context 'when the primary scanner of the other is not nil' do + before do + report_2.add_scanner(scanner_2) + end + + it { is_expected.to be(1) } + end + end + + context 'when the primary scanner of the receiver is not nil' do + before do + report_1.add_scanner(scanner_1) + end + + context 'when the primary scanner of the other is nil' do + let(:scanner_2) { nil } + + it { is_expected.to be(-1) } + end + + context 'when the primary scanner of the other is not nil' do + before do + report_2.add_scanner(scanner_2) + + allow(scanner_1).to receive(:<=>).and_return(0) + end + + it 'compares two scanners' do + expect(compare_based_on_primary_scanners).to be(0) + expect(scanner_1).to have_received(:<=>).with(scanner_2) + end + end + end + end +end diff --git a/spec/lib/gitlab/ci/reports/security/reports_spec.rb b/spec/lib/gitlab/ci/reports/security/reports_spec.rb new file mode 100644 index 00000000000..9b1e02f1418 --- /dev/null +++ b/spec/lib/gitlab/ci/reports/security/reports_spec.rb @@ -0,0 +1,113 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Reports::Security::Reports do + let_it_be(:pipeline) { create(:ci_pipeline) } + let_it_be(:artifact) { create(:ci_job_artifact, :sast) } + + let(:security_reports) { described_class.new(pipeline) } + + describe '#get_report' do + subject { security_reports.get_report(report_type, artifact) } + + context 'when report type is sast' do + let(:report_type) { 'sast' } + + it { expect(subject.type).to eq('sast') } + it { expect(subject.created_at).to eq(artifact.created_at) } + + it 'initializes a new report and returns it' do + expect(Gitlab::Ci::Reports::Security::Report).to receive(:new) + .with('sast', pipeline, artifact.created_at).and_call_original + + is_expected.to be_a(Gitlab::Ci::Reports::Security::Report) + end + + context 'when report type is already allocated' do + before do + subject + end + + it 'does not initialize a new report' do + expect(Gitlab::Ci::Reports::Security::Report).not_to receive(:new) + + is_expected.to be_a(Gitlab::Ci::Reports::Security::Report) + end + end + end + end + + describe '#findings' do + let(:finding_1) { build(:ci_reports_security_finding, severity: 'low') } + let(:finding_2) { build(:ci_reports_security_finding, severity: 'high') } + let!(:expected_findings) { [finding_1, finding_2] } + + subject { security_reports.findings } + + before do + security_reports.get_report('sast', artifact).add_finding(finding_1) + security_reports.get_report('dependency_scanning', artifact).add_finding(finding_2) + end + + it { is_expected.to match_array(expected_findings) } + end + + describe "#violates_default_policy_against?" do + let(:high_severity_dast) { build(:ci_reports_security_finding, severity: 'high', report_type: :dast) } + let(:vulnerabilities_allowed) { 0 } + let(:severity_levels) { %w(critical high) } + + subject { security_reports.violates_default_policy_against?(target_reports, vulnerabilities_allowed, severity_levels) } + + before do + security_reports.get_report('sast', artifact).add_finding(high_severity_dast) + end + + context 'when the target_reports is `nil`' do + let(:target_reports) { nil } + + context 'with severity levels matching the existing vulnerabilities' do + it { is_expected.to be(true) } + end + + context "without any severity levels matching the existing vulnerabilities" do + let(:severity_levels) { %w(critical) } + + it { is_expected.to be(false) } + end + end + + context 'when the target_reports is not `nil`' do + let(:target_reports) { described_class.new(pipeline) } + + context "when a report has a new unsafe vulnerability" do + context 'with severity levels matching the existing vulnerabilities' do + it { is_expected.to be(true) } + end + + it { is_expected.to be(true) } + + context 'with vulnerabilities_allowed higher than the number of new vulnerabilities' do + let(:vulnerabilities_allowed) { 10000 } + + it { is_expected.to be(false) } + end + + context "without any severity levels matching the existing vulnerabilities" do + let(:severity_levels) { %w(critical) } + + it { is_expected.to be(false) } + end + end + + context "when none of the reports have a new unsafe vulnerability" do + before do + target_reports.get_report('sast', artifact).add_finding(high_severity_dast) + end + + it { is_expected.to be(false) } + end + end + end +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 new file mode 100644 index 00000000000..44e66fd9028 --- /dev/null +++ b/spec/lib/gitlab/ci/reports/security/vulnerability_reports_comparer_spec.rb @@ -0,0 +1,163 @@ +# 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/templates/5_minute_production_app_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/5_minute_production_app_ci_yaml_spec.rb index 6bc8e261640..f8df2266689 100644 --- a/spec/lib/gitlab/ci/templates/5_minute_production_app_ci_yaml_spec.rb +++ b/spec/lib/gitlab/ci/templates/5_minute_production_app_ci_yaml_spec.rb @@ -12,7 +12,7 @@ RSpec.describe '5-Minute-Production-App.gitlab-ci.yml' do let(:default_branch) { 'master' } let(:pipeline_branch) { default_branch } let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_branch ) } - let(:pipeline) { service.execute!(:push) } + let(:pipeline) { service.execute!(:push).payload } let(:build_names) { pipeline.builds.pluck(:name) } before do diff --git a/spec/lib/gitlab/ci/templates/AWS/deploy_ecs_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/AWS/deploy_ecs_gitlab_ci_yaml_spec.rb index e8aeb93a2ba..ca6f6872f89 100644 --- a/spec/lib/gitlab/ci/templates/AWS/deploy_ecs_gitlab_ci_yaml_spec.rb +++ b/spec/lib/gitlab/ci/templates/AWS/deploy_ecs_gitlab_ci_yaml_spec.rb @@ -11,7 +11,7 @@ RSpec.describe 'Deploy-ECS.gitlab-ci.yml' do let(:project) { create(:project, :auto_devops, :custom_repo, files: { 'README.md' => '' }) } let(:user) { project.owner } let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_branch ) } - let(:pipeline) { service.execute!(:push) } + let(:pipeline) { service.execute!(:push).payload } let(:build_names) { pipeline.builds.pluck(:name) } let(:platform_target) { 'ECS' } 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 053499344e1..bd701aec8fc 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 @@ -12,7 +12,7 @@ RSpec.describe 'Jobs/Build.gitlab-ci.yml' do let(:default_branch) { 'master' } let(:pipeline_ref) { default_branch } let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_ref) } - let(:pipeline) { service.execute!(:push) } + let(:pipeline) { service.execute!(:push).payload } let(:build_names) { pipeline.builds.pluck(:name) } before do @@ -47,7 +47,7 @@ RSpec.describe 'Jobs/Build.gitlab-ci.yml' do 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) } + let(:pipeline) { service.execute(merge_request).payload } it 'has no jobs' do expect(pipeline).to be_merge_request_event diff --git a/spec/lib/gitlab/ci/templates/Jobs/code_quality_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/Jobs/code_quality_gitlab_ci_yaml_spec.rb index b23457315cc..64243f2d205 100644 --- a/spec/lib/gitlab/ci/templates/Jobs/code_quality_gitlab_ci_yaml_spec.rb +++ b/spec/lib/gitlab/ci/templates/Jobs/code_quality_gitlab_ci_yaml_spec.rb @@ -12,7 +12,7 @@ RSpec.describe 'Jobs/Code-Quality.gitlab-ci.yml' do let(:default_branch) { 'master' } let(:pipeline_ref) { default_branch } let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_ref) } - let(:pipeline) { service.execute!(:push) } + let(:pipeline) { service.execute!(:push).payload } let(:build_names) { pipeline.builds.pluck(:name) } before do @@ -47,7 +47,7 @@ RSpec.describe 'Jobs/Code-Quality.gitlab-ci.yml' do 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) } + let(:pipeline) { service.execute(merge_request).payload } it 'has no jobs' do expect(pipeline).to be_merge_request_event diff --git a/spec/lib/gitlab/ci/templates/Jobs/deploy_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/Jobs/deploy_gitlab_ci_yaml_spec.rb index 1d137ef89e1..d377cf0c735 100644 --- a/spec/lib/gitlab/ci/templates/Jobs/deploy_gitlab_ci_yaml_spec.rb +++ b/spec/lib/gitlab/ci/templates/Jobs/deploy_gitlab_ci_yaml_spec.rb @@ -33,7 +33,7 @@ RSpec.describe 'Jobs/Deploy.gitlab-ci.yml' do let(:default_branch) { 'master' } let(:pipeline_ref) { default_branch } let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_ref) } - let(:pipeline) { service.execute!(:push) } + let(:pipeline) { service.execute!(:push).payload } let(:build_names) { pipeline.builds.pluck(:name) } before do @@ -210,7 +210,7 @@ RSpec.describe 'Jobs/Deploy.gitlab-ci.yml' do 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) } + let(:pipeline) { service.execute(merge_request).payload } it 'has no jobs' do expect(pipeline).to be_merge_request_event diff --git a/spec/lib/gitlab/ci/templates/Jobs/test_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/Jobs/test_gitlab_ci_yaml_spec.rb index 7fa8d906d07..db9d7496251 100644 --- a/spec/lib/gitlab/ci/templates/Jobs/test_gitlab_ci_yaml_spec.rb +++ b/spec/lib/gitlab/ci/templates/Jobs/test_gitlab_ci_yaml_spec.rb @@ -12,7 +12,7 @@ RSpec.describe 'Jobs/Test.gitlab-ci.yml' do let(:default_branch) { 'master' } let(:pipeline_ref) { default_branch } let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_ref) } - let(:pipeline) { service.execute!(:push) } + let(:pipeline) { service.execute!(:push).payload } let(:build_names) { pipeline.builds.pluck(:name) } before do @@ -47,7 +47,7 @@ RSpec.describe 'Jobs/Test.gitlab-ci.yml' do 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) } + let(:pipeline) { service.execute(merge_request).payload } it 'has no jobs' do expect(pipeline).to be_merge_request_event diff --git a/spec/lib/gitlab/ci/templates/Terraform/base_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/Terraform/base_gitlab_ci_yaml_spec.rb index 0811c07e896..4685d843ce0 100644 --- a/spec/lib/gitlab/ci/templates/Terraform/base_gitlab_ci_yaml_spec.rb +++ b/spec/lib/gitlab/ci/templates/Terraform/base_gitlab_ci_yaml_spec.rb @@ -2,8 +2,8 @@ require 'spec_helper' -RSpec.describe 'Terraform/Base.latest.gitlab-ci.yml' do - subject(:template) { Gitlab::Template::GitlabCiYmlTemplate.find('Terraform/Base.latest') } +RSpec.describe 'Terraform/Base.gitlab-ci.yml' do + subject(:template) { Gitlab::Template::GitlabCiYmlTemplate.find('Terraform/Base') } describe 'the created pipeline' do let(:default_branch) { 'master' } @@ -11,7 +11,7 @@ RSpec.describe 'Terraform/Base.latest.gitlab-ci.yml' do let(:project) { create(:project, :custom_repo, files: { 'README.md' => '' }) } let(:user) { project.owner } let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_branch ) } - let(:pipeline) { service.execute!(:push) } + let(:pipeline) { service.execute!(:push).payload } let(:build_names) { pipeline.builds.pluck(:name) } before do diff --git a/spec/lib/gitlab/ci/templates/Terraform/base_latest_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/Terraform/base_latest_gitlab_ci_yaml_spec.rb new file mode 100644 index 00000000000..e35f2eabe8e --- /dev/null +++ b/spec/lib/gitlab/ci/templates/Terraform/base_latest_gitlab_ci_yaml_spec.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Terraform/Base.latest.gitlab-ci.yml' do + subject(:template) { Gitlab::Template::GitlabCiYmlTemplate.find('Terraform/Base.latest') } + + describe 'the created pipeline' do + let(:default_branch) { 'master' } + let(:pipeline_branch) { default_branch } + let(:project) { create(:project, :custom_repo, files: { 'README.md' => '' }) } + let(:user) { project.owner } + let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_branch ) } + let(:pipeline) { service.execute!(:push).payload } + let(:build_names) { pipeline.builds.pluck(:name) } + + before do + stub_ci_pipeline_yaml_file(template.content) + allow(project).to receive(:default_branch).and_return(default_branch) + end + + it 'does not create any jobs' do + expect(build_names).to be_empty + end + end +end diff --git a/spec/lib/gitlab/ci/templates/Verify/load_performance_testing_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/Verify/load_performance_testing_gitlab_ci_yaml_spec.rb index e53d2f4f975..004261bc617 100644 --- a/spec/lib/gitlab/ci/templates/Verify/load_performance_testing_gitlab_ci_yaml_spec.rb +++ b/spec/lib/gitlab/ci/templates/Verify/load_performance_testing_gitlab_ci_yaml_spec.rb @@ -25,7 +25,7 @@ RSpec.describe 'Verify/Load-Performance-Testing.gitlab-ci.yml' do let(:default_branch) { 'master' } let(:pipeline_ref) { default_branch } let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_ref) } - let(:pipeline) { service.execute!(:push) } + let(:pipeline) { service.execute!(:push).payload } let(:build_names) { pipeline.builds.pluck(:name) } before do @@ -64,7 +64,7 @@ RSpec.describe 'Verify/Load-Performance-Testing.gitlab-ci.yml' do 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) } + let(:pipeline) { service.execute(merge_request).payload } it 'has no jobs' do expect(pipeline).to be_merge_request_event diff --git a/spec/lib/gitlab/ci/templates/auto_devops_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/auto_devops_gitlab_ci_yaml_spec.rb index b40b4f5645f..7602309627b 100644 --- a/spec/lib/gitlab/ci/templates/auto_devops_gitlab_ci_yaml_spec.rb +++ b/spec/lib/gitlab/ci/templates/auto_devops_gitlab_ci_yaml_spec.rb @@ -17,7 +17,7 @@ RSpec.describe 'Auto-DevOps.gitlab-ci.yml' do let(:project) { create(:project, :auto_devops, :custom_repo, files: { 'README.md' => '' }) } let(:user) { project.owner } let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_branch ) } - let(:pipeline) { service.execute!(:push) } + let(:pipeline) { service.execute!(:push).payload } let(:build_names) { pipeline.builds.pluck(:name) } before do @@ -264,7 +264,7 @@ RSpec.describe 'Auto-DevOps.gitlab-ci.yml' do let(:project) { create(:project, :custom_repo, files: files) } let(:user) { project.owner } let(:service) { Ci::CreatePipelineService.new(project, user, ref: default_branch ) } - let(:pipeline) { service.execute(:push) } + let(:pipeline) { service.execute(:push).payload } let(:build_names) { pipeline.builds.pluck(:name) } before do diff --git a/spec/lib/gitlab/ci/templates/flutter_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/flutter_gitlab_ci_yaml_spec.rb index 4e5fe622648..3d97b47473d 100644 --- a/spec/lib/gitlab/ci/templates/flutter_gitlab_ci_yaml_spec.rb +++ b/spec/lib/gitlab/ci/templates/flutter_gitlab_ci_yaml_spec.rb @@ -10,7 +10,7 @@ RSpec.describe 'Flutter.gitlab-ci.yml' do let(:project) { create(:project, :custom_repo, files: { 'README.md' => '' }) } let(:user) { project.owner } let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_branch ) } - let(:pipeline) { service.execute!(:push) } + let(:pipeline) { service.execute!(:push).payload } let(:build_names) { pipeline.builds.pluck(:name) } before do diff --git a/spec/lib/gitlab/ci/templates/managed_cluster_applications_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/managed_cluster_applications_gitlab_ci_yaml_spec.rb index 151880e27a3..14aaf717453 100644 --- a/spec/lib/gitlab/ci/templates/managed_cluster_applications_gitlab_ci_yaml_spec.rb +++ b/spec/lib/gitlab/ci/templates/managed_cluster_applications_gitlab_ci_yaml_spec.rb @@ -10,7 +10,7 @@ RSpec.describe 'Managed-Cluster-Applications.gitlab-ci.yml' do let(:project) { create(:project, :custom_repo, namespace: user.namespace, files: { 'README.md' => '' }) } let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_branch ) } - let(:pipeline) { service.execute!(:push) } + let(:pipeline) { service.execute!(:push).payload } let(:build_names) { pipeline.builds.pluck(:name) } let(:default_branch) { project.default_branch_or_main } let(:pipeline_branch) { default_branch } diff --git a/spec/lib/gitlab/ci/templates/npm_spec.rb b/spec/lib/gitlab/ci/templates/npm_spec.rb index 2456c9ae545..ea954690133 100644 --- a/spec/lib/gitlab/ci/templates/npm_spec.rb +++ b/spec/lib/gitlab/ci/templates/npm_spec.rb @@ -14,7 +14,7 @@ RSpec.describe 'npm.gitlab-ci.yml' do let(:pipeline_tag) { 'v1.2.1' } let(:pipeline_ref) { pipeline_branch } let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_ref ) } - let(:pipeline) { service.execute!(:push) } + let(:pipeline) { service.execute!(:push).payload } let(:build_names) { pipeline.builds.pluck(:name) } def create_branch(name:) diff --git a/spec/lib/gitlab/ci/templates/terraform_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/terraform_gitlab_ci_yaml_spec.rb new file mode 100644 index 00000000000..936cd6ac8aa --- /dev/null +++ b/spec/lib/gitlab/ci/templates/terraform_gitlab_ci_yaml_spec.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Terraform.gitlab-ci.yml' do + before do + allow(Gitlab::Template::GitlabCiYmlTemplate).to receive(:excluded_patterns).and_return([]) + end + + subject(:template) { Gitlab::Template::GitlabCiYmlTemplate.find('Terraform') } + + describe 'the created pipeline' do + let(:default_branch) { project.default_branch_or_main } + let(:pipeline_branch) { default_branch } + let(:project) { create(:project, :custom_repo, files: { 'README.md' => '' }) } + let(:user) { project.owner } + let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_branch ) } + let(:pipeline) { service.execute!(:push).payload } + let(:build_names) { pipeline.builds.pluck(:name) } + + before do + stub_ci_pipeline_yaml_file(template.content) + allow(project).to receive(:default_branch).and_return(default_branch) + end + + context 'on master branch' do + it 'creates init, validate and build jobs', :aggregate_failures do + expect(pipeline.errors).to be_empty + expect(build_names).to include('init', 'validate', 'build', 'deploy') + end + end + + context 'outside the master branch' do + let(:pipeline_branch) { 'patch-1' } + + before do + project.repository.create_branch(pipeline_branch, default_branch) + end + + it 'does not creates a deploy and a test job', :aggregate_failures do + expect(pipeline.errors).to be_empty + expect(build_names).not_to include('deploy') + end + end + end +end diff --git a/spec/lib/gitlab/ci/templates/terraform_latest_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/terraform_latest_gitlab_ci_yaml_spec.rb index 5ab3035486f..3d1306e82a5 100644 --- a/spec/lib/gitlab/ci/templates/terraform_latest_gitlab_ci_yaml_spec.rb +++ b/spec/lib/gitlab/ci/templates/terraform_latest_gitlab_ci_yaml_spec.rb @@ -15,7 +15,7 @@ RSpec.describe 'Terraform.latest.gitlab-ci.yml' do let(:project) { create(:project, :custom_repo, files: { 'README.md' => '' }) } let(:user) { project.owner } let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_branch ) } - let(:pipeline) { service.execute!(:push) } + let(:pipeline) { service.execute!(:push).payload } let(:build_names) { pipeline.builds.pluck(:name) } before do @@ -25,7 +25,8 @@ RSpec.describe 'Terraform.latest.gitlab-ci.yml' do end context 'on master branch' do - it 'creates init, validate and build jobs' do + it 'creates init, validate and build jobs', :aggregate_failures do + expect(pipeline.errors).to be_empty expect(build_names).to include('init', 'validate', 'build', 'deploy') end end @@ -37,7 +38,8 @@ RSpec.describe 'Terraform.latest.gitlab-ci.yml' do project.repository.create_branch(pipeline_branch, default_branch) end - it 'does not creates a deploy and a test job' do + it 'does not creates a deploy and a test job', :aggregate_failures do + expect(pipeline.errors).to be_empty expect(build_names).not_to include('deploy') end end diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb index 19c2e34a0f0..49a470f9e01 100644 --- a/spec/lib/gitlab/ci/yaml_processor_spec.rb +++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb @@ -42,7 +42,6 @@ module Gitlab interruptible: true, allow_failure: false, when: "on_success", - yaml_variables: [], job_variables: [], root_variables_inheritance: true, scheduling_type: :stage @@ -75,7 +74,6 @@ module Gitlab ], allow_failure: false, when: 'on_success', - yaml_variables: [], job_variables: [], root_variables_inheritance: true, scheduling_type: :stage @@ -115,7 +113,6 @@ module Gitlab tag_list: %w[A B], allow_failure: false, when: "on_success", - yaml_variables: [], job_variables: [], root_variables_inheritance: true }) @@ -163,7 +160,6 @@ module Gitlab interruptible: true, allow_failure: false, when: "on_success", - yaml_variables: [], job_variables: [], root_variables_inheritance: true, scheduling_type: :stage @@ -354,7 +350,6 @@ module Gitlab name: "rspec", allow_failure: false, when: "on_success", - yaml_variables: [], job_variables: [], root_variables_inheritance: true, scheduling_type: :stage, @@ -368,7 +363,6 @@ module Gitlab name: "prod", allow_failure: false, when: "on_success", - yaml_variables: [], job_variables: [], root_variables_inheritance: true, scheduling_type: :stage, @@ -847,7 +841,6 @@ module Gitlab }, allow_failure: false, when: "on_success", - yaml_variables: [], job_variables: [], root_variables_inheritance: true, scheduling_type: :stage @@ -882,7 +875,6 @@ module Gitlab }, allow_failure: false, when: "on_success", - yaml_variables: [], job_variables: [], root_variables_inheritance: true, scheduling_type: :stage @@ -913,7 +905,6 @@ module Gitlab }, allow_failure: false, when: "on_success", - yaml_variables: [], job_variables: [], root_variables_inheritance: true, scheduling_type: :stage @@ -942,7 +933,6 @@ module Gitlab }, allow_failure: false, when: "on_success", - yaml_variables: [], job_variables: [], root_variables_inheritance: true, scheduling_type: :stage @@ -955,7 +945,6 @@ module Gitlab subject { Gitlab::Ci::YamlProcessor.new(YAML.dump(config)).execute } let(:build) { subject.builds.first } - let(:yaml_variables) { build[:yaml_variables] } let(:job_variables) { build[:job_variables] } let(:root_variables_inheritance) { build[:root_variables_inheritance] } @@ -973,84 +962,11 @@ module Gitlab end it 'returns global variables' do - expect(yaml_variables).to contain_exactly( - { key: 'VAR1', value: 'value1', public: true }, - { key: 'VAR2', value: 'value2', public: true } - ) expect(job_variables).to eq([]) expect(root_variables_inheritance).to eq(true) end end - context 'when job and global variables are defined' do - let(:global_variables) do - { 'VAR1' => 'global1', 'VAR3' => 'global3', 'VAR4' => 'global4' } - end - - let(:build_variables) do - { 'VAR1' => 'value1', 'VAR2' => 'value2' } - end - - let(:config) do - { - before_script: ['pwd'], - variables: global_variables, - rspec: { script: 'rspec', variables: build_variables, inherit: inherit } - } - end - - context 'when no inheritance is specified' do - let(:inherit) { } - - it 'returns all variables' do - expect(yaml_variables).to contain_exactly( - { key: 'VAR1', value: 'value1', public: true }, - { key: 'VAR2', value: 'value2', public: true }, - { key: 'VAR3', value: 'global3', public: true }, - { key: 'VAR4', value: 'global4', public: true } - ) - expect(job_variables).to contain_exactly( - { key: 'VAR1', value: 'value1', public: true }, - { key: 'VAR2', value: 'value2', public: true } - ) - expect(root_variables_inheritance).to eq(true) - end - end - - context 'when inheritance is disabled' do - let(:inherit) { { variables: false } } - - it 'does not inherit variables' do - expect(yaml_variables).to contain_exactly( - { key: 'VAR1', value: 'value1', public: true }, - { key: 'VAR2', value: 'value2', public: true } - ) - expect(job_variables).to contain_exactly( - { key: 'VAR1', value: 'value1', public: true }, - { key: 'VAR2', value: 'value2', public: true } - ) - expect(root_variables_inheritance).to eq(false) - end - end - - context 'when specific variables are to inherited' do - let(:inherit) { { variables: %w[VAR1 VAR4] } } - - it 'returns all variables and inherits only specified variables' do - expect(yaml_variables).to contain_exactly( - { key: 'VAR1', value: 'value1', public: true }, - { key: 'VAR2', value: 'value2', public: true }, - { key: 'VAR4', value: 'global4', public: true } - ) - expect(job_variables).to contain_exactly( - { key: 'VAR1', value: 'value1', public: true }, - { key: 'VAR2', value: 'value2', public: true } - ) - expect(root_variables_inheritance).to eq(%w[VAR1 VAR4]) - end - end - end - context 'when job variables are defined' do let(:config) do { @@ -1065,10 +981,6 @@ module Gitlab end it 'returns job variables' do - expect(yaml_variables).to contain_exactly( - { key: 'VAR1', value: 'value1', public: true }, - { key: 'VAR2', value: 'value2', public: true } - ) expect(job_variables).to contain_exactly( { key: 'VAR1', value: 'value1', public: true }, { key: 'VAR2', value: 'value2', public: true } @@ -1096,9 +1008,6 @@ module Gitlab # When variables config is empty, we assume this is a valid # configuration, see issue #18775 # - expect(yaml_variables).to be_an_instance_of(Array) - expect(yaml_variables).to be_empty - expect(job_variables).to eq([]) expect(root_variables_inheritance).to eq(true) end @@ -1115,9 +1024,6 @@ module Gitlab end it 'returns empty array' do - expect(yaml_variables).to be_an_instance_of(Array) - expect(yaml_variables).to be_empty - expect(job_variables).to eq([]) expect(root_variables_inheritance).to eq(true) end @@ -1246,6 +1152,10 @@ module Gitlab end it { is_expected.to be_valid } + + it 'adds the job from the included file' do + expect(subject.builds.map { |build| build[:name] }).to contain_exactly('job1', 'rspec') + end end context "when the included internal file is not present" do @@ -1349,7 +1259,7 @@ module Gitlab end it 'sets matrix variables' do - build_variables = builds.map { |build| build[:yaml_variables] } + build_variables = builds.map { |build| build[:job_variables] } expected_variables = [ [ { key: 'VAR1', value: '1' }, @@ -1601,7 +1511,6 @@ module Gitlab }, when: "on_success", allow_failure: false, - yaml_variables: [], job_variables: [], root_variables_inheritance: true, scheduling_type: :stage @@ -1972,7 +1881,6 @@ module Gitlab }, when: 'on_success', allow_failure: false, - yaml_variables: [], job_variables: [], root_variables_inheritance: true, scheduling_type: :stage @@ -1988,7 +1896,6 @@ module Gitlab ], when: 'on_success', allow_failure: false, - yaml_variables: [], job_variables: [], root_variables_inheritance: true, scheduling_type: :dag @@ -2011,7 +1918,6 @@ module Gitlab }, when: "on_success", allow_failure: false, - yaml_variables: [], job_variables: [], root_variables_inheritance: true, scheduling_type: :stage @@ -2028,7 +1934,6 @@ module Gitlab ], when: "on_success", allow_failure: false, - yaml_variables: [], job_variables: [], root_variables_inheritance: true, scheduling_type: :dag @@ -2057,7 +1962,6 @@ module Gitlab }, when: "on_success", allow_failure: false, - yaml_variables: [], job_variables: [], root_variables_inheritance: true, scheduling_type: :stage @@ -2076,7 +1980,6 @@ module Gitlab ], when: "on_success", allow_failure: false, - yaml_variables: [], job_variables: [], root_variables_inheritance: true, scheduling_type: :dag @@ -2101,7 +2004,6 @@ module Gitlab ], when: "on_success", allow_failure: false, - yaml_variables: [], job_variables: [], root_variables_inheritance: true, scheduling_type: :dag @@ -2134,7 +2036,6 @@ module Gitlab ], when: "on_success", allow_failure: false, - yaml_variables: [], job_variables: [], root_variables_inheritance: true, scheduling_type: :dag @@ -2342,7 +2243,6 @@ module Gitlab }, when: "on_success", allow_failure: false, - yaml_variables: [], job_variables: [], root_variables_inheritance: true, scheduling_type: :stage @@ -2391,7 +2291,6 @@ module Gitlab }, when: "on_success", allow_failure: false, - yaml_variables: [], job_variables: [], root_variables_inheritance: true, scheduling_type: :stage @@ -2406,7 +2305,6 @@ module Gitlab }, when: "on_success", allow_failure: false, - yaml_variables: [], job_variables: [], root_variables_inheritance: true, scheduling_type: :stage @@ -2851,7 +2749,7 @@ module Gitlab YAML end - it_behaves_like 'returns errors', 'The pipeline has circular dependencies.' + it_behaves_like 'returns errors', 'The pipeline has circular dependencies' end end @@ -2883,7 +2781,7 @@ module Gitlab expect(subject.valid?).to eq(false) expect(subject.errors).to contain_exactly( 'jobs:rspec config contains unknown keys: bad_tags', - 'jobs:rspec rules should be an array of hashes') + 'jobs:rspec rules should be an array containing hashes and arrays of hashes') end end |