diff options
Diffstat (limited to 'spec/lib/gitlab/ci')
45 files changed, 1590 insertions, 874 deletions
diff --git a/spec/lib/gitlab/ci/ansi2json_spec.rb b/spec/lib/gitlab/ci/ansi2json_spec.rb index 4b3b049176f..0f8f3759834 100644 --- a/spec/lib/gitlab/ci/ansi2json_spec.rb +++ b/spec/lib/gitlab/ci/ansi2json_spec.rb @@ -7,70 +7,74 @@ RSpec.describe Gitlab::Ci::Ansi2json do describe 'lines' do it 'prints non-ansi as-is' do - expect(convert_json('Hello')).to eq([ - { offset: 0, content: [{ text: 'Hello' }] } - ]) + expect(convert_json('Hello')).to eq([{ offset: 0, content: [{ text: 'Hello' }] }]) end context 'new lines' do it 'adds new line when encountering \n' do - expect(convert_json("Hello\nworld")).to eq([ - { offset: 0, content: [{ text: 'Hello' }] }, - { offset: 6, content: [{ text: 'world' }] } - ]) + expect(convert_json("Hello\nworld")).to eq( + [ + { offset: 0, content: [{ text: 'Hello' }] }, + { offset: 6, content: [{ text: 'world' }] } + ]) end it 'adds new line when encountering \r\n' do - expect(convert_json("Hello\r\nworld")).to eq([ - { offset: 0, content: [{ text: 'Hello' }] }, - { offset: 7, content: [{ text: 'world' }] } - ]) + expect(convert_json("Hello\r\nworld")).to eq( + [ + { offset: 0, content: [{ text: 'Hello' }] }, + { offset: 7, content: [{ text: 'world' }] } + ]) end it 'ignores empty newlines' do - expect(convert_json("Hello\n\nworld")).to eq([ - { offset: 0, content: [{ text: 'Hello' }] }, - { offset: 7, content: [{ text: 'world' }] } - ]) - expect(convert_json("Hello\r\n\r\nworld")).to eq([ - { offset: 0, content: [{ text: 'Hello' }] }, - { offset: 9, content: [{ text: 'world' }] } - ]) + expect(convert_json("Hello\n\nworld")).to eq( + [ + { offset: 0, content: [{ text: 'Hello' }] }, + { offset: 7, content: [{ text: 'world' }] } + ]) + expect(convert_json("Hello\r\n\r\nworld")).to eq( + [ + { offset: 0, content: [{ text: 'Hello' }] }, + { offset: 9, content: [{ text: 'world' }] } + ]) end it 'replace the current line when encountering \r' do - expect(convert_json("Hello\rworld")).to eq([ - { offset: 0, content: [{ text: 'world' }] } - ]) + expect(convert_json("Hello\rworld")).to eq([{ offset: 0, content: [{ text: 'world' }] }]) end end it 'recognizes color changing ANSI sequences' do - expect(convert_json("\e[31mHello\e[0m")).to eq([ - { offset: 0, content: [{ text: 'Hello', style: 'term-fg-red' }] } - ]) + expect(convert_json("\e[31mHello\e[0m")).to eq( + [ + { offset: 0, content: [{ text: 'Hello', style: 'term-fg-red' }] } + ]) end it 'recognizes color changing ANSI sequences across multiple lines' do - expect(convert_json("\e[31mHello\nWorld\e[0m")).to eq([ - { offset: 0, content: [{ text: 'Hello', style: 'term-fg-red' }] }, - { offset: 11, content: [{ text: 'World', style: 'term-fg-red' }] } - ]) + expect(convert_json("\e[31mHello\nWorld\e[0m")).to eq( + [ + { offset: 0, content: [{ text: 'Hello', style: 'term-fg-red' }] }, + { offset: 11, content: [{ text: 'World', style: 'term-fg-red' }] } + ]) end it 'recognizes background and foreground colors' do - expect(convert_json("\e[31;44mHello")).to eq([ - { offset: 0, content: [{ text: 'Hello', style: 'term-fg-red term-bg-blue' }] } - ]) + expect(convert_json("\e[31;44mHello")).to eq( + [ + { offset: 0, content: [{ text: 'Hello', style: 'term-fg-red term-bg-blue' }] } + ]) end it 'recognizes style changes within the same line' do - expect(convert_json("\e[31;44mHello\e[0m world")).to eq([ - { offset: 0, content: [ - { text: 'Hello', style: 'term-fg-red term-bg-blue' }, - { text: ' world' } - ] } - ]) + expect(convert_json("\e[31;44mHello\e[0m world")).to eq( + [ + { offset: 0, content: [ + { text: 'Hello', style: 'term-fg-red term-bg-blue' }, + { text: ' world' } + ] } + ]) end context 'with section markers' do @@ -82,130 +86,137 @@ RSpec.describe Gitlab::Ci::Ansi2json do let(:section_end) { "section_end:#{section_end_time.to_i}:#{section_name}\r\033[0K" } it 'marks the first line of the section as header' do - expect(convert_json("Hello#{section_start}world!")).to eq([ - { - offset: 0, - content: [{ text: 'Hello' }] - }, - { - offset: 5, - content: [{ text: 'world!' }], - section: 'prepare-script', - section_header: true - } - ]) + expect(convert_json("Hello#{section_start}world!")).to eq( + [ + { + offset: 0, + content: [{ text: 'Hello' }] + }, + { + offset: 5, + content: [{ text: 'world!' }], + section: 'prepare-script', + section_header: true + } + ]) end it 'does not marks the other lines of the section as header' do - expect(convert_json("outside section#{section_start}Hello\nworld!")).to eq([ - { - offset: 0, - content: [{ text: 'outside section' }] - }, - { - offset: 15, - content: [{ text: 'Hello' }], - section: 'prepare-script', - section_header: true - }, - { - offset: 65, - content: [{ text: 'world!' }], - section: 'prepare-script' - } - ]) + expect(convert_json("outside section#{section_start}Hello\nworld!")).to eq( + [ + { + offset: 0, + content: [{ text: 'outside section' }] + }, + { + offset: 15, + content: [{ text: 'Hello' }], + section: 'prepare-script', + section_header: true + }, + { + offset: 65, + content: [{ text: 'world!' }], + section: 'prepare-script' + } + ]) end it 'marks the last line of the section as footer' do - expect(convert_json("#{section_start}Good\nmorning\nworld!#{section_end}")).to eq([ - { - offset: 0, - content: [{ text: 'Good' }], - section: 'prepare-script', - section_header: true - }, - { - offset: 49, - content: [{ text: 'morning' }], - section: 'prepare-script' - }, - { - offset: 57, - content: [{ text: 'world!' }], - section: 'prepare-script' - }, - { - offset: 63, - content: [], - section_duration: '01:03', - section: 'prepare-script' - } - ]) + expect(convert_json("#{section_start}Good\nmorning\nworld!#{section_end}")).to eq( + [ + { + offset: 0, + content: [{ text: 'Good' }], + section: 'prepare-script', + section_header: true + }, + { + offset: 49, + content: [{ text: 'morning' }], + section: 'prepare-script' + }, + { + offset: 57, + content: [{ text: 'world!' }], + section: 'prepare-script' + }, + { + offset: 63, + content: [], + section_duration: '01:03', + section: 'prepare-script' + } + ]) end it 'marks the first line as header and footer if is the only line in the section' do - expect(convert_json("#{section_start}Hello world!#{section_end}")).to eq([ - { - offset: 0, - content: [{ text: 'Hello world!' }], - section: 'prepare-script', - section_header: true - }, - { - offset: 56, - content: [], - section: 'prepare-script', - section_duration: '01:03' - } - ]) + expect(convert_json("#{section_start}Hello world!#{section_end}")).to eq( + [ + { + offset: 0, + content: [{ text: 'Hello world!' }], + section: 'prepare-script', + section_header: true + }, + { + offset: 56, + content: [], + section: 'prepare-script', + section_duration: '01:03' + } + ]) end it 'does not add sections attribute to lines after the section is closed' do - expect(convert_json("#{section_start}Hello#{section_end}world")).to eq([ - { - offset: 0, - content: [{ text: 'Hello' }], - section: 'prepare-script', - section_header: true - }, - { - offset: 49, - content: [], - section: 'prepare-script', - section_duration: '01:03' - }, - { - offset: 91, - content: [{ text: 'world' }] - } - ]) + expect(convert_json("#{section_start}Hello#{section_end}world")).to eq( + [ + { + offset: 0, + content: [{ text: 'Hello' }], + section: 'prepare-script', + section_header: true + }, + { + offset: 49, + content: [], + section: 'prepare-script', + section_duration: '01:03' + }, + { + offset: 91, + content: [{ text: 'world' }] + } + ]) end it 'ignores section_end marker if no section_start exists' do - expect(convert_json("Hello #{section_end}world")).to eq([ - { - offset: 0, - content: [{ text: 'Hello world' }] - } - ]) + expect(convert_json("Hello #{section_end}world")).to eq( + [ + { + offset: 0, + content: [{ text: 'Hello world' }] + } + ]) end context 'when section name contains .-_ and capital letters' do let(:section_name) { 'a.Legit-SeCtIoN_namE' } it 'sanitizes the section name' do - expect(convert_json("Hello#{section_start}world!")).to eq([ - { - offset: 0, - content: [{ text: 'Hello' }] - }, - { - offset: 5, - content: [{ text: 'world!' }], - section: 'a-legit-section-name', - section_header: true - } - ]) + expect(convert_json("Hello#{section_start}world!")).to eq( + [ + { + offset: 0, + content: [{ text: 'Hello' }] + }, + { + offset: 5, + content: [{ text: 'world!' }], + section: 'a-legit-section-name', + section_header: true + } + ]) end end @@ -213,12 +224,13 @@ RSpec.describe Gitlab::Ci::Ansi2json do let(:section_name) { 'my_$ection' } it 'ignores the section' do - expect(convert_json("#{section_start}hello")).to eq([ - { - offset: 0, - content: [{ text: 'hello' }] - } - ]) + expect(convert_json("#{section_start}hello")).to eq( + [ + { + offset: 0, + content: [{ text: 'hello' }] + } + ]) end end @@ -226,31 +238,33 @@ RSpec.describe Gitlab::Ci::Ansi2json do let(:section_name) { '<a_tag>' } it 'ignores the section' do - expect(convert_json("#{section_start}hello")).to eq([ - { - offset: 0, - content: [{ text: 'hello' }] - } - ]) + expect(convert_json("#{section_start}hello")).to eq( + [ + { + offset: 0, + content: [{ text: 'hello' }] + } + ]) end end it 'prints HTML tags as is' do trace = "#{section_start}section_end:1:2<div>hello</div>#{section_end}" - expect(convert_json(trace)).to eq([ - { - offset: 0, - content: [{ text: 'section_end:1:2<div>hello</div>' }], - section: 'prepare-script', - section_header: true - }, - { - offset: 75, - content: [], - section: 'prepare-script', - section_duration: '01:03' - } - ]) + expect(convert_json(trace)).to eq( + [ + { + offset: 0, + content: [{ text: 'section_end:1:2<div>hello</div>' }], + section: 'prepare-script', + section_header: true + }, + { + offset: 75, + content: [], + section: 'prepare-script', + section_duration: '01:03' + } + ]) end context 'with nested section' do @@ -264,7 +278,8 @@ RSpec.describe Gitlab::Ci::Ansi2json do it 'adds multiple sections to the lines inside the nested section' do trace = "Hello#{section_start}foo#{nested_section_start}bar#{nested_section_end}baz#{section_end}world" - expect(convert_json(trace)).to eq([ + expect(convert_json(trace)).to eq( + [ { offset: 0, content: [{ text: 'Hello' }] @@ -308,7 +323,8 @@ RSpec.describe Gitlab::Ci::Ansi2json do it 'adds multiple sections to the lines inside the nested section and closes all sections together' do trace = "Hello#{section_start}\e[91mfoo\e[0m#{nested_section_start}bar#{nested_section_end}#{section_end}" - expect(convert_json(trace)).to eq([ + expect(convert_json(trace)).to eq( + [ { offset: 0, content: [{ text: 'Hello' }] @@ -346,24 +362,25 @@ RSpec.describe Gitlab::Ci::Ansi2json do it 'provides section options when set' do trace = "#{option_section_start}hello#{section_end}" - expect(convert_json(trace)).to eq([ - { - offset: 0, - content: [{ text: 'hello' }], - section: 'prepare-script', - section_header: true, - section_options: { - 'collapsed' => 'true', - 'unused_option' => '123' + expect(convert_json(trace)).to eq( + [ + { + offset: 0, + content: [{ text: 'hello' }], + section: 'prepare-script', + section_header: true, + section_options: { + 'collapsed' => 'true', + 'unused_option' => '123' + } + }, + { + offset: 83, + content: [], + section: 'prepare-script', + section_duration: '01:03' } - }, - { - offset: 83, - content: [], - section: 'prepare-script', - section_duration: '01:03' - } - ]) + ]) end end end diff --git a/spec/lib/gitlab/ci/build/rules/rule/clause/changes_spec.rb b/spec/lib/gitlab/ci/build/rules/rule/clause/changes_spec.rb index 234ba68d627..a22aa30304b 100644 --- a/spec/lib/gitlab/ci/build/rules/rule/clause/changes_spec.rb +++ b/spec/lib/gitlab/ci/build/rules/rule/clause/changes_spec.rb @@ -122,19 +122,17 @@ RSpec.describe Gitlab::Ci::Build::Rules::Rule::Clause::Changes do context 'when compare_to is branch or tag' do using RSpec::Parameterized::TableSyntax - where(:pipeline_ref, :compare_to, :paths, :ff, :result) do - 'feature_1' | 'master' | ['file1.txt'] | true | true - 'feature_1' | 'master' | ['README.md'] | true | false - 'feature_1' | 'master' | ['xyz.md'] | true | false - 'feature_2' | 'master' | ['file1.txt'] | true | true - 'feature_2' | 'master' | ['file2.txt'] | true | true - 'feature_2' | 'feature_1' | ['file1.txt'] | true | false - 'feature_2' | 'feature_1' | ['file1.txt'] | false | true - 'feature_2' | 'feature_1' | ['file2.txt'] | true | true - 'feature_1' | 'tag_1' | ['file1.txt'] | true | false - 'feature_1' | 'tag_1' | ['file1.txt'] | false | true - 'feature_1' | 'tag_1' | ['file2.txt'] | true | true - 'feature_2' | 'tag_1' | ['file2.txt'] | true | true + where(:pipeline_ref, :compare_to, :paths, :result) do + 'feature_1' | 'master' | ['file1.txt'] | true + 'feature_1' | 'master' | ['README.md'] | false + 'feature_1' | 'master' | ['xyz.md'] | false + 'feature_2' | 'master' | ['file1.txt'] | true + 'feature_2' | 'master' | ['file2.txt'] | true + 'feature_2' | 'feature_1' | ['file1.txt'] | false + 'feature_2' | 'feature_1' | ['file2.txt'] | true + 'feature_1' | 'tag_1' | ['file1.txt'] | false + 'feature_1' | 'tag_1' | ['file2.txt'] | true + 'feature_2' | 'tag_1' | ['file2.txt'] | true end with_them do @@ -144,10 +142,6 @@ RSpec.describe Gitlab::Ci::Build::Rules::Rule::Clause::Changes do build(:ci_pipeline, project: project, ref: pipeline_ref, sha: project.commit(pipeline_ref).sha) end - before do - stub_feature_flags(ci_rules_changes_compare: ff) - end - it { is_expected.to eq(result) } end end @@ -174,14 +168,6 @@ RSpec.describe Gitlab::Ci::Build::Rules::Rule::Clause::Changes do ::Gitlab::Ci::Build::Rules::Rule::Clause::ParseError, 'rules:changes:compare_to is not a valid ref' ) end - - context 'when the FF ci_rules_changes_compare is disabled' do - before do - stub_feature_flags(ci_rules_changes_compare: false) - end - - it { is_expected.to be_truthy } - end end end end diff --git a/spec/lib/gitlab/ci/config/entry/legacy_variables_spec.rb b/spec/lib/gitlab/ci/config/entry/legacy_variables_spec.rb deleted file mode 100644 index e9edec9a0a4..00000000000 --- a/spec/lib/gitlab/ci/config/entry/legacy_variables_spec.rb +++ /dev/null @@ -1,173 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe Gitlab::Ci::Config::Entry::LegacyVariables do - let(:config) { {} } - let(:metadata) { {} } - - subject(:entry) { described_class.new(config, **metadata) } - - before do - entry.compose! - end - - shared_examples 'valid config' do - describe '#value' do - it 'returns hash with key value strings' do - expect(entry.value).to eq result - end - end - - describe '#errors' do - it 'does not append errors' do - expect(entry.errors).to be_empty - end - end - - describe '#valid?' do - it 'is valid' do - expect(entry).to be_valid - end - end - end - - shared_examples 'invalid config' do |error_message| - describe '#valid?' do - it 'is not valid' do - expect(entry).not_to be_valid - end - end - - describe '#errors' do - it 'saves errors' do - expect(entry.errors) - .to include(error_message) - end - end - end - - context 'when entry config value has key-value pairs' do - let(:config) do - { 'VARIABLE_1' => 'value 1', 'VARIABLE_2' => 'value 2' } - end - - let(:result) do - { 'VARIABLE_1' => 'value 1', 'VARIABLE_2' => 'value 2' } - end - - it_behaves_like 'valid config' - - describe '#value_with_data' do - it 'returns variable with data' do - expect(entry.value_with_data).to eq( - 'VARIABLE_1' => { value: 'value 1' }, - 'VARIABLE_2' => { value: 'value 2' } - ) - end - end - end - - context 'with numeric keys and values in the config' do - let(:config) { { 10 => 20 } } - let(:result) do - { '10' => '20' } - end - - it_behaves_like 'valid config' - end - - context 'when key is an array' do - let(:config) { { ['VAR1'] => 'val1' } } - let(:result) do - { 'VAR1' => 'val1' } - end - - it_behaves_like 'invalid config', /should be a hash of key value pairs/ - end - - context 'when value is a symbol' do - let(:config) { { 'VAR1' => :val1 } } - let(:result) do - { 'VAR1' => 'val1' } - end - - it_behaves_like 'valid config' - end - - context 'when value is a boolean' do - let(:config) { { 'VAR1' => true } } - let(:result) do - { 'VAR1' => 'val1' } - end - - it_behaves_like 'invalid config', /should be a hash of key value pairs/ - end - - context 'when entry config value has key-value pair and hash' do - let(:config) do - { 'VARIABLE_1' => { value: 'value 1', description: 'variable 1' }, - 'VARIABLE_2' => 'value 2' } - end - - it_behaves_like 'invalid config', /should be a hash of key value pairs/ - - context 'when metadata has use_value_data: true' do - let(:metadata) { { use_value_data: true } } - - let(:result) do - { 'VARIABLE_1' => 'value 1', 'VARIABLE_2' => 'value 2' } - end - - it_behaves_like 'valid config' - - describe '#value_with_data' do - it 'returns variable with data' do - expect(entry.value_with_data).to eq( - 'VARIABLE_1' => { value: 'value 1', description: 'variable 1' }, - 'VARIABLE_2' => { value: 'value 2' } - ) - end - end - end - end - - context 'when entry value is an array' do - let(:config) { [:VAR, 'test'] } - - it_behaves_like 'invalid config', /should be a hash of key value pairs/ - end - - context 'when metadata has use_value_data: true' do - let(:metadata) { { use_value_data: true } } - - context 'when entry value has hash with other key-pairs' do - let(:config) do - { 'VARIABLE_1' => { value: 'value 1', hello: 'variable 1' }, - 'VARIABLE_2' => 'value 2' } - end - - it_behaves_like 'invalid config', /should be a hash of key value pairs, value can be a hash/ - end - - context 'when entry config value has hash with nil description' do - let(:config) do - { 'VARIABLE_1' => { value: 'value 1', description: nil } } - end - - it_behaves_like 'invalid config', /should be a hash of key value pairs, value can be a hash/ - end - - context 'when entry config value has hash without description' do - let(:config) do - { 'VARIABLE_1' => { value: 'value 1' } } - end - - let(:result) do - { 'VARIABLE_1' => 'value 1' } - end - - it_behaves_like 'valid config' - end - end -end diff --git a/spec/lib/gitlab/ci/config/entry/processable_spec.rb b/spec/lib/gitlab/ci/config/entry/processable_spec.rb index 5f42a8c49a7..ad90dd59585 100644 --- a/spec/lib/gitlab/ci/config/entry/processable_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/processable_spec.rb @@ -210,20 +210,6 @@ RSpec.describe Gitlab::Ci::Config::Entry::Processable do expect(entry.errors) .to include 'variables:var2 config must be a string' end - - context 'when the FF ci_variables_refactoring_to_variable is disabled' do - let(:entry_without_ff) { node_class.new(config, name: :rspec) } - - before do - stub_feature_flags(ci_variables_refactoring_to_variable: false) - entry_without_ff.compose! - end - - it 'reports error about variable' do - expect(entry_without_ff.errors) - .to include /config should be a hash of key value pairs/ - end - end end end end diff --git a/spec/lib/gitlab/ci/config/entry/product/parallel_spec.rb b/spec/lib/gitlab/ci/config/entry/product/parallel_spec.rb index 937642f07e7..a16f1cf9e43 100644 --- a/spec/lib/gitlab/ci/config/entry/product/parallel_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/product/parallel_spec.rb @@ -91,10 +91,11 @@ RSpec.describe ::Gitlab::Ci::Config::Entry::Product::Parallel do describe '#value' do it 'returns job needs configuration' do - expect(parallel.value).to match(matrix: [ - { PROVIDER: 'aws', STACK: %w[monitoring app1 app2] }, - { PROVIDER: 'gcp', STACK: %w[data processing] } - ]) + expect(parallel.value).to match(matrix: + [ + { PROVIDER: 'aws', STACK: %w[monitoring app1 app2] }, + { PROVIDER: 'gcp', STACK: %w[data processing] } + ]) end end diff --git a/spec/lib/gitlab/ci/config/entry/root_spec.rb b/spec/lib/gitlab/ci/config/entry/root_spec.rb index 3d19987a0be..a55e13e7c2d 100644 --- a/spec/lib/gitlab/ci/config/entry/root_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/root_spec.rb @@ -34,7 +34,11 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do image: 'image:1.0', default: {}, services: ['postgres:9.1', 'mysql:5.5'], - variables: { VAR: 'root', VAR2: { value: 'val 2', description: 'this is var 2' } }, + variables: { + VAR: 'root', + VAR2: { value: 'val 2', description: 'this is var 2' }, + VAR3: { value: %w[val3 val3b], description: 'this is var 3' } + }, after_script: ['make clean'], stages: %w(build pages release), cache: { key: 'k', untracked: true, paths: ['public/'] }, @@ -83,7 +87,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do end it 'sets correct variables value' do - expect(root.variables_value).to eq('VAR' => 'root', 'VAR2' => 'val 2') + expect(root.variables_value).to eq('VAR' => 'root', 'VAR2' => 'val 2', 'VAR3' => 'val3') end describe '#leaf?' do @@ -361,20 +365,6 @@ RSpec.describe Gitlab::Ci::Config::Entry::Root do expect(root.errors) .to include /var1 config uses invalid data keys: invalid/ end - - context 'when the FF ci_variables_refactoring_to_variable is disabled' do - let(:root_without_ff) { described_class.new(hash, user: user, project: project) } - - before do - stub_feature_flags(ci_variables_refactoring_to_variable: false) - root_without_ff.compose! - end - - it 'reports errors about the invalid variable' do - expect(root_without_ff.errors) - .to include /variables config should be a hash of key value pairs, value can be a hash/ - end - end end end end diff --git a/spec/lib/gitlab/ci/config/entry/rules/rule_spec.rb b/spec/lib/gitlab/ci/config/entry/rules/rule_spec.rb index 303d825c591..3531d6e9f1a 100644 --- a/spec/lib/gitlab/ci/config/entry/rules/rule_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/rules/rule_spec.rb @@ -364,19 +364,6 @@ RSpec.describe Gitlab::Ci::Config::Entry::Rules::Rule do it 'returns an error about invalid variables:' do expect(subject.errors).to include(/variables config should be a hash/) end - - context 'when the FF ci_variables_refactoring_to_variable is disabled' do - let(:entry_without_ff) { factory.create! } - - before do - stub_feature_flags(ci_variables_refactoring_to_variable: false) - entry_without_ff.compose! - end - - it 'returns an error about invalid variables:' do - expect(subject.errors).to include(/variables config should be a hash/) - end - end end end diff --git a/spec/lib/gitlab/ci/config/entry/variable_spec.rb b/spec/lib/gitlab/ci/config/entry/variable_spec.rb index 744a89d4509..076a5b32e92 100644 --- a/spec/lib/gitlab/ci/config/entry/variable_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/variable_spec.rb @@ -127,20 +127,6 @@ RSpec.describe Gitlab::Ci::Config::Entry::Variable do end end - context 'when config value is an array' do - let(:config) { { value: ['value'], description: 'description' } } - - describe '#valid?' do - it { is_expected.not_to be_valid } - end - - describe '#errors' do - subject(:errors) { entry.errors } - - it { is_expected.to include 'var1 config value must be an alphanumeric string' } - end - end - context 'when config description is a symbol' do let(:config) { { value: 'value', description: :description } } @@ -209,4 +195,42 @@ RSpec.describe Gitlab::Ci::Config::Entry::Variable do end end end + + describe 'ComplexArrayVariable' do + context 'when allow_array_value metadata is false' do + let(:config) { { value: %w[value value2], description: 'description' } } + let(:metadata) { { allow_array_value: false } } + + describe '#valid?' do + it { is_expected.not_to be_valid } + end + + describe '#errors' do + subject(:errors) { entry.errors } + + it { is_expected.to include 'var1 config value must be an alphanumeric string' } + end + end + + context 'when allow_array_value metadata is true' do + let(:config) { { value: %w[value value2], description: 'description' } } + let(:metadata) { { allowed_value_data: %i[value description], allow_array_value: true } } + + describe '#valid?' do + it { is_expected.to be_valid } + end + + describe '#value' do + subject(:value) { entry.value } + + it { is_expected.to eq('value') } + end + + describe '#value_with_data' do + subject(:value_with_data) { entry.value_with_data } + + it { is_expected.to eq(value: 'value', description: 'description', value_options: %w[value value2]) } + end + end + end end diff --git a/spec/lib/gitlab/ci/config/entry/variables_spec.rb b/spec/lib/gitlab/ci/config/entry/variables_spec.rb index ad7290d0589..085f304094e 100644 --- a/spec/lib/gitlab/ci/config/entry/variables_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/variables_spec.rb @@ -98,6 +98,62 @@ RSpec.describe Gitlab::Ci::Config::Entry::Variables do it_behaves_like 'invalid config', /must be either a string or a hash/ end + context 'when entry config value has unallowed value key-value pair and value is a string' do + let(:config) do + { 'VARIABLE_1' => { value: 'value', description: 'variable 1' } } + end + + context 'when there is no allowed_value_data metadata' do + it_behaves_like 'invalid config', /variable_1 config must be a string/ + end + + context 'when metadata has allow_array_value and allowed_value_data' do + let(:metadata) { { allowed_value_data: %i[value description], allow_array_value: true } } + + let(:result) do + { 'VARIABLE_1' => 'value' } + end + + it_behaves_like 'valid config' + + describe '#value_with_data' do + it 'returns variable with data' do + expect(entry.value_with_data).to eq( + 'VARIABLE_1' => { value: 'value', description: 'variable 1' } + ) + end + end + end + end + + context 'when entry config value has key-value pair and value is an array' do + let(:config) do + { 'VARIABLE_1' => { value: %w[value1 value2], description: 'variable 1' } } + end + + context 'when there is no allowed_value_data metadata' do + it_behaves_like 'invalid config', /variable_1 config value must be an alphanumeric string/ + end + + context 'when metadata has allow_array_value and allowed_value_data' do + let(:metadata) { { allowed_value_data: %i[value description], allow_array_value: true } } + + let(:result) do + { 'VARIABLE_1' => 'value1' } + end + + it_behaves_like 'valid config' + + describe '#value_with_data' do + it 'returns variable with data' do + expect(entry.value_with_data).to eq( + 'VARIABLE_1' => { value: 'value1', value_options: %w[value1 value2], description: 'variable 1' } + ) + end + end + end + end + context 'when entry config value has key-value pair and hash' do let(:config) do { 'VARIABLE_1' => { value: 'value 1', description: 'variable 1' }, diff --git a/spec/lib/gitlab/ci/config/entry/workflow_spec.rb b/spec/lib/gitlab/ci/config/entry/workflow_spec.rb index 3d19832e13d..97ac199f47d 100644 --- a/spec/lib/gitlab/ci/config/entry/workflow_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/workflow_spec.rb @@ -65,6 +65,54 @@ RSpec.describe Gitlab::Ci::Config::Entry::Workflow do end end end + + context 'with workflow name' do + let(:factory) { Gitlab::Config::Entry::Factory.new(described_class).value(workflow_hash) } + + context 'with a blank name' do + let(:workflow_hash) do + { name: '' } + end + + it 'is invalid' do + expect(config).not_to be_valid + end + + it 'returns error about invalid name' do + expect(config.errors).to include('workflow name is too short (minimum is 1 character)') + end + end + + context 'with too long name' do + let(:workflow_hash) do + { name: 'a' * 256 } + end + + it 'is invalid' do + expect(config).not_to be_valid + end + + it 'returns error about invalid name' do + expect(config.errors).to include('workflow name is too long (maximum is 255 characters)') + end + end + + context 'when name is nil' do + let(:workflow_hash) { { name: nil } } + + it 'is valid' do + expect(config).to be_valid + end + end + + context 'when name is not provided' do + let(:workflow_hash) { { rules: [{ if: '$VAR' }] } } + + it 'is valid' do + expect(config).to be_valid + end + end + end end end diff --git a/spec/lib/gitlab/ci/config/external/file/artifact_spec.rb b/spec/lib/gitlab/ci/config/external/file/artifact_spec.rb index 9da8d106862..a8dc7897082 100644 --- a/spec/lib/gitlab/ci/config/external/file/artifact_spec.rb +++ b/spec/lib/gitlab/ci/config/external/file/artifact_spec.rb @@ -174,9 +174,10 @@ RSpec.describe Gitlab::Ci::Config::External::File::Artifact do context 'when job is provided as a variable' do let(:variables) do - Gitlab::Ci::Variables::Collection.new([ - { key: 'VAR1', value: 'a_secret_variable_value', masked: true } - ]) + Gitlab::Ci::Variables::Collection.new( + [ + { key: 'VAR1', value: 'a_secret_variable_value', masked: true } + ]) end let(:params) { { artifact: 'generated.yml', job: 'a_secret_variable_value' } } diff --git a/spec/lib/gitlab/ci/config/external/file/project_spec.rb b/spec/lib/gitlab/ci/config/external/file/project_spec.rb index 72a85c9b03d..0ba92d1e92d 100644 --- a/spec/lib/gitlab/ci/config/external/file/project_spec.rb +++ b/spec/lib/gitlab/ci/config/external/file/project_spec.rb @@ -163,9 +163,7 @@ RSpec.describe Gitlab::Ci::Config::External::File::Project do context 'when non-existing project is used with a masked variable' do let(:variables) do - Gitlab::Ci::Variables::Collection.new([ - { key: 'VAR1', value: 'a_secret_variable_value', masked: true } - ]) + Gitlab::Ci::Variables::Collection.new([{ key: 'VAR1', value: 'a_secret_variable_value', masked: true }]) end let(:params) do @@ -180,9 +178,7 @@ RSpec.describe Gitlab::Ci::Config::External::File::Project do context 'when a project contained in an array is used with a masked variable' do let(:variables) do - Gitlab::Ci::Variables::Collection.new([ - { key: 'VAR1', value: 'a_secret_variable_value', masked: true } - ]) + Gitlab::Ci::Variables::Collection.new([{ key: 'VAR1', value: 'a_secret_variable_value', masked: true }]) end let(:params) do @@ -231,10 +227,11 @@ RSpec.describe Gitlab::Ci::Config::External::File::Project do context 'when project name and ref include masked variables' do let(:variables) do - Gitlab::Ci::Variables::Collection.new([ - { key: 'VAR1', value: 'a_secret_variable_value1', masked: true }, - { key: 'VAR2', value: 'a_secret_variable_value2', masked: true } - ]) + Gitlab::Ci::Variables::Collection.new( + [ + { key: 'VAR1', value: 'a_secret_variable_value1', masked: true }, + { key: 'VAR2', value: 'a_secret_variable_value2', masked: true } + ]) end let(:params) { { project: 'a_secret_variable_value1', ref: 'a_secret_variable_value2', file: '/file.yml' } } diff --git a/spec/lib/gitlab/ci/config/external/mapper_spec.rb b/spec/lib/gitlab/ci/config/external/mapper_spec.rb index 9eaba12f388..e12f5dcee0a 100644 --- a/spec/lib/gitlab/ci/config/external/mapper_spec.rb +++ b/spec/lib/gitlab/ci/config/external/mapper_spec.rb @@ -207,9 +207,9 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper do context "when duplicate 'include's are defined" do let(:values) do { include: [ - { 'local' => local_file }, - { 'local' => local_file } - ], + { 'local' => local_file }, + { 'local' => local_file } + ], image: 'image:1.0' } end @@ -416,17 +416,18 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper do context "when locations are same after masking variables" do let(:variables) do - Gitlab::Ci::Variables::Collection.new([ - { 'key' => 'GITLAB_TOKEN', 'value' => 'secret-file1', 'masked' => true }, - { 'key' => 'GITLAB_TOKEN', 'value' => 'secret-file2', 'masked' => true } - ]) + Gitlab::Ci::Variables::Collection.new( + [ + { 'key' => 'GITLAB_TOKEN', 'value' => 'secret-file1', 'masked' => true }, + { 'key' => 'GITLAB_TOKEN', 'value' => 'secret-file2', 'masked' => true } + ]) end let(:values) do { include: [ - { 'local' => 'hello/secret-file1.yml' }, - { 'local' => 'hello/secret-file2.yml' } - ], + { 'local' => 'hello/secret-file1.yml' }, + { 'local' => 'hello/secret-file2.yml' } + ], image: 'ruby:2.7' } end diff --git a/spec/lib/gitlab/ci/config_spec.rb b/spec/lib/gitlab/ci/config_spec.rb index 055114769ea..475503de7da 100644 --- a/spec/lib/gitlab/ci/config_spec.rb +++ b/spec/lib/gitlab/ci/config_spec.rb @@ -889,4 +889,31 @@ RSpec.describe Gitlab::Ci::Config do it { is_expected.to eq([{ if: '$CI_COMMIT_REF_NAME == "master"' }]) } end + + describe '#workflow_name' do + subject(:workflow_name) { config.workflow_name } + + let(:yml) do + <<-EOS + workflow: + name: 'Pipeline name' + + rspec: + script: exit 0 + EOS + end + + it { is_expected.to eq('Pipeline name') } + + context 'with no name' do + let(:yml) do + <<-EOS + rspec: + script: exit 0 + EOS + end + + it { is_expected.to be_nil } + end + end end diff --git a/spec/lib/gitlab/ci/jwt_v2_spec.rb b/spec/lib/gitlab/ci/jwt_v2_spec.rb index 33aaa145a39..5eeab658a8e 100644 --- a/spec/lib/gitlab/ci/jwt_v2_spec.rb +++ b/spec/lib/gitlab/ci/jwt_v2_spec.rb @@ -7,6 +7,8 @@ RSpec.describe Gitlab::Ci::JwtV2 do let(:project) { build_stubbed(:project, namespace: namespace) } let(:user) { build_stubbed(:user) } let(:pipeline) { build_stubbed(:ci_pipeline, ref: 'auto-deploy-2020-03-19') } + let(:aud) { described_class::DEFAULT_AUD } + let(:build) do build_stubbed( :ci_build, @@ -16,7 +18,7 @@ RSpec.describe Gitlab::Ci::JwtV2 do ) end - subject(:ci_job_jwt_v2) { described_class.new(build, ttl: 30) } + subject(:ci_job_jwt_v2) { described_class.new(build, ttl: 30, aud: aud) } it { is_expected.to be_a Gitlab::Ci::Jwt } @@ -30,5 +32,13 @@ RSpec.describe Gitlab::Ci::JwtV2 do expect(payload[:sub]).to eq("project_path:#{project.full_path}:ref_type:branch:ref:#{pipeline.source_ref}") end end + + context 'when given an aud' do + let(:aud) { 'AWS' } + + it 'uses that aud in the payload' do + expect(payload[:aud]).to eq('AWS') + end + end end end diff --git a/spec/lib/gitlab/ci/lint_spec.rb b/spec/lib/gitlab/ci/lint_spec.rb index 3d46d266c13..cf07e952f26 100644 --- a/spec/lib/gitlab/ci/lint_spec.rb +++ b/spec/lib/gitlab/ci/lint_spec.rb @@ -342,6 +342,7 @@ RSpec.describe Gitlab::Ci::Lint do { 'count' => a_kind_of(Numeric), 'avg' => a_kind_of(Numeric), + 'sum' => a_kind_of(Numeric), 'max' => a_kind_of(Numeric), 'min' => a_kind_of(Numeric) } diff --git a/spec/lib/gitlab/ci/parsers/sbom/source/dependency_scanning_spec.rb b/spec/lib/gitlab/ci/parsers/sbom/source/dependency_scanning_spec.rb index 7222ebc3cb8..e12fa380209 100644 --- a/spec/lib/gitlab/ci/parsers/sbom/source/dependency_scanning_spec.rb +++ b/spec/lib/gitlab/ci/parsers/sbom/source/dependency_scanning_spec.rb @@ -19,8 +19,7 @@ RSpec.describe Gitlab::Ci::Parsers::Sbom::Source::DependencyScanning do it 'returns expected source data' do is_expected.to have_attributes( source_type: :dependency_scanning, - data: property_data, - fingerprint: '4dbcb747e6f0fb3ed4f48d96b777f1d64acdf43e459fdfefad404e55c004a188' + data: property_data ) end end diff --git a/spec/lib/gitlab/ci/parsers/sbom/validators/cyclonedx_schema_validator_spec.rb b/spec/lib/gitlab/ci/parsers/sbom/validators/cyclonedx_schema_validator_spec.rb index c54a3268bbe..f58a463f047 100644 --- a/spec/lib/gitlab/ci/parsers/sbom/validators/cyclonedx_schema_validator_spec.rb +++ b/spec/lib/gitlab/ci/parsers/sbom/validators/cyclonedx_schema_validator_spec.rb @@ -72,12 +72,13 @@ RSpec.describe Gitlab::Ci::Parsers::Sbom::Validators::CyclonedxSchemaValidator d it { is_expected.not_to be_valid } it "outputs errors for each validation failure" do - expect(validator.errors).to match_array([ - "property '/components/0' is missing required keys: name", - "property '/components/0/type' is not one of: [\"application\", \"framework\"," \ - " \"library\", \"container\", \"operating-system\", \"device\", \"firmware\", \"file\"]", - "property '/components/1' is missing required keys: type" - ]) + expect(validator.errors).to match_array( + [ + "property '/components/0' is missing required keys: name", + "property '/components/0/type' is not one of: [\"application\", \"framework\"," \ + " \"library\", \"container\", \"operating-system\", \"device\", \"firmware\", \"file\"]", + "property '/components/1' is missing required keys: type" + ]) end end end @@ -121,10 +122,11 @@ RSpec.describe Gitlab::Ci::Parsers::Sbom::Validators::CyclonedxSchemaValidator d it { is_expected.not_to be_valid } it "outputs errors for each validation failure" do - expect(validator.errors).to match_array([ - "property '/metadata/properties/0/name' is not of type: string", - "property '/metadata/properties/0/value' is not of type: string" - ]) + expect(validator.errors).to match_array( + [ + "property '/metadata/properties/0/name' is not of type: string", + "property '/metadata/properties/0/value' is not of type: string" + ]) end end end diff --git a/spec/lib/gitlab/ci/parsers/security/common_spec.rb b/spec/lib/gitlab/ci/parsers/security/common_spec.rb index 297ef1f5bb9..7dbad354e4c 100644 --- a/spec/lib/gitlab/ci/parsers/security/common_spec.rb +++ b/spec/lib/gitlab/ci/parsers/security/common_spec.rb @@ -54,24 +54,15 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Common do context 'when the validate flag is set to `false`' do let(:validate) { false } - let(:valid?) { false } - let(:errors) { ['foo'] } - let(:warnings) { ['bar'] } 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(errors) - allow(instance).to receive(:warnings).and_return(warnings) - end - allow(parser).to receive_messages(create_scanner: true, create_scan: true) end - it 'instantiates the validator with correct params' do + it 'does not instantiate the validator' do parse_report - expect(validator_class).to have_received(:new).with( + expect(validator_class).not_to have_received(:new).with( report.type, data.deep_stringify_keys, report.version, @@ -80,43 +71,17 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Common do ) end - context 'when the report data is not valid according to the schema' do - it 'adds warnings to the report' do - expect { parse_report }.to change { report.warnings }.from([]).to( - [ - { message: 'foo', type: 'Schema' }, - { message: 'bar', type: 'Schema' } - ] - ) - end - - it 'keeps the execution flow as normal' do - parse_report + it 'marks the report as valid' do + parse_report - expect(parser).to have_received(:create_scanner) - expect(parser).to have_received(:create_scan) - end + expect(report).not_to be_errored end - context 'when the report data is valid according to the schema' do - let(:valid?) { true } - let(:errors) { [] } - let(:warnings) { [] } - - it 'does not add errors to the report' do - expect { parse_report }.not_to change { report.errors } - end - - it 'does not add warnings to the report' do - expect { parse_report }.not_to change { report.warnings } - end - - it 'keeps the execution flow as normal' do - parse_report + 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 + expect(parser).to have_received(:create_scanner) + expect(parser).to have_received(:create_scan) end end @@ -152,12 +117,17 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Common do it 'adds errors to the report' do expect { parse_report }.to change { report.errors }.from([]).to( [ - { message: 'foo', type: 'Schema' }, - { message: 'bar', type: 'Schema' } + { message: 'foo', type: 'Schema' } ] ) end + it 'marks the report as invalid' do + parse_report + + expect(report).to be_errored + end + it 'does not try to create report entities' do parse_report @@ -175,8 +145,24 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Common do expect { parse_report }.not_to change { report.errors }.from([]) end - it 'does not add warnings to the report' do - expect { parse_report }.not_to change { report.warnings }.from([]) + context 'and no warnings are present' do + let(:warnings) { [] } + + it 'does not add warnings to the report' do + expect { parse_report }.not_to change { report.warnings }.from([]) + end + end + + context 'and some warnings are present' do + let(:warnings) { ['bar'] } + + it 'does add warnings to the report' do + expect { parse_report }.to change { report.warnings }.from([]).to( + [ + { message: 'bar', type: 'Schema' } + ] + ) + end end it 'keeps the execution flow as normal' do @@ -298,8 +284,8 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Common 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.map(&:start_time).all?('2022-08-10T21:37:00')).to be(true) + expect(scans.map(&:end_time).all?('2022-08-10T21:38:00')).to be(true) expect(scans.size).to eq(7) expect(scans.first).to be_a(::Gitlab::Ci::Reports::Security::Scan) end @@ -418,11 +404,11 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Common do { 'type' => 'source', 'items' => [ - 'signatures' => [ - { 'algorithm' => 'hash', 'value' => 'hash_value' }, - { 'algorithm' => 'location', 'value' => 'location_value' }, - { 'algorithm' => 'scope_offset', 'value' => 'scope_offset_value' } - ] + 'signatures' => [ + { 'algorithm' => 'hash', 'value' => 'hash_value' }, + { 'algorithm' => 'location', 'value' => 'location_value' }, + { 'algorithm' => 'scope_offset', 'value' => 'scope_offset_value' } + ] ] } end @@ -440,11 +426,11 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Common do { 'type' => 'source', 'items' => [ - 'signatures' => [ - { 'algorithm' => 'hash', 'value' => 'hash_value' }, - { 'algorithm' => 'location', 'value' => 'location_value' }, - { 'algorithm' => 'INVALID', 'value' => 'scope_offset_value' } - ] + 'signatures' => [ + { 'algorithm' => 'hash', 'value' => 'hash_value' }, + { 'algorithm' => 'location', 'value' => 'location_value' }, + { 'algorithm' => 'INVALID', 'value' => 'scope_offset_value' } + ] ] } end diff --git a/spec/lib/gitlab/ci/parsers/security/sast_spec.rb b/spec/lib/gitlab/ci/parsers/security/sast_spec.rb index 4bc48f6611a..f6113308201 100644 --- a/spec/lib/gitlab/ci/parsers/security/sast_spec.rb +++ b/spec/lib/gitlab/ci/parsers/security/sast_spec.rb @@ -10,24 +10,39 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Sast do 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 + context "when passing valid report" do + # rubocop: disable Layout/LineLength + where(:report_format, :report_version, :scanner_length, :finding_length, :identifier_length, :file_path, :start_line, :end_line, :primary_identifiers_length) do + :sast | '14.0.0' | 1 | 5 | 6 | 'groovy/src/main/java/com/gitlab/security_products/tests/App.groovy' | 47 | 47 | nil + :sast_semgrep_for_multiple_findings | '14.0.4' | 1 | 2 | 6 | 'app/app.py' | 39 | nil | 2 end + # rubocop: enable Layout/LineLength with_them do - let(:report) { Gitlab::Ci::Reports::Security::Report.new(artifact.file_type, pipeline, created_at) } + let(:report) do + Gitlab::Ci::Reports::Security::Report.new( + artifact.file_type, + pipeline, + created_at + ) + end + let(:artifact) { create(:ci_job_artifact, report_format) } before do - artifact.each_blob { |blob| described_class.parse!(blob, report) } + artifact.each_blob { |blob| described_class.parse!(blob, report, validate: true) } 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) + + if primary_identifiers_length + expect( + report.scanners.each_value.first.primary_identifiers.length + ).to eq(primary_identifiers_length) + end end it 'generates expected location' do @@ -36,8 +51,8 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Sast do 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_line: end_line, + start_line: start_line ) end diff --git a/spec/lib/gitlab/ci/pipeline/chain/cancel_pending_pipelines_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/cancel_pending_pipelines_spec.rb index b570f2a7f75..fc3de2a14cd 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/cancel_pending_pipelines_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/cancel_pending_pipelines_spec.rb @@ -44,6 +44,20 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::CancelPendingPipelines do expect(build_statuses(pipeline)).to contain_exactly('pending') end + it 'logs canceled pipelines' do + allow(Gitlab::AppLogger).to receive(:info) + + perform + + expect(Gitlab::AppLogger).to have_received(:info).with( + class: described_class.name, + message: "Pipeline #{pipeline.id} auto-canceling pipeline #{prev_pipeline.id}", + canceled_pipeline_id: prev_pipeline.id, + canceled_by_pipeline_id: pipeline.id, + canceled_by_pipeline_source: pipeline.source + ) + end + it 'cancels the builds with 2 queries to avoid query timeout' do second_query_regex = /WHERE "ci_pipelines"\."id" = \d+ AND \(NOT EXISTS/ recorder = ActiveRecord::QueryRecorder.new { perform } @@ -141,7 +155,42 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::CancelPendingPipelines do end end - context 'when the prev pipeline source is webide' do + context 'when the pipeline is a child pipeline' do + let!(:parent_pipeline) { create(:ci_pipeline, project: project, sha: new_commit.sha) } + let(:pipeline) { create(:ci_pipeline, child_of: parent_pipeline) } + + before do + create(:ci_build, :interruptible, :running, pipeline: parent_pipeline) + create(:ci_build, :interruptible, :running, pipeline: parent_pipeline) + end + + it 'does not cancel any builds' do + expect(build_statuses(prev_pipeline)).to contain_exactly('running', 'success', 'created') + expect(build_statuses(parent_pipeline)).to contain_exactly('running', 'running') + + perform + + expect(build_statuses(prev_pipeline)).to contain_exactly('running', 'success', 'created') + expect(build_statuses(parent_pipeline)).to contain_exactly('running', 'running') + end + + context 'when feature flag ci_skip_auto_cancelation_on_child_pipelines is disabled' do + before do + stub_feature_flags(ci_skip_auto_cancelation_on_child_pipelines: false) + end + + it 'does not cancel the parent pipeline' do + expect(build_statuses(parent_pipeline)).to contain_exactly('running', 'running') + + perform + + expect(build_statuses(prev_pipeline)).to contain_exactly('success', 'canceled', 'canceled') + expect(build_statuses(parent_pipeline)).to contain_exactly('running', 'running') + end + end + end + + context 'when the previous pipeline source is webide' do let(:prev_pipeline) { create(:ci_pipeline, :webide, project: project) } it 'does not cancel builds of the previous pipeline' do diff --git a/spec/lib/gitlab/ci/pipeline/chain/config/content_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/config/content_spec.rb index f451bd6bfef..e0d656f456e 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/config/content_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/config/content_spec.rb @@ -11,9 +11,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Config::Content do subject { described_class.new(pipeline, command) } - # TODO: change this to `describe` and remove rubocop-disable - # when removing the FF ci_project_pipeline_config_refactoring - shared_context '#perform!' do # rubocop:disable RSpec/ContextWording + describe '#perform!' do context 'when bridge job is passed in as parameter' do let(:ci_config_path) { nil } let(:bridge) { create(:ci_bridge) } @@ -203,14 +201,4 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Config::Content do end end end - - it_behaves_like '#perform!' - - context 'when the FF ci_project_pipeline_config_refactoring is disabled' do - before do - stub_feature_flags(ci_project_pipeline_config_refactoring: false) - end - - it_behaves_like '#perform!' - end end diff --git a/spec/lib/gitlab/ci/pipeline/chain/limit/active_jobs_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/limit/active_jobs_spec.rb new file mode 100644 index 00000000000..bc453f1502b --- /dev/null +++ b/spec/lib/gitlab/ci/pipeline/chain/limit/active_jobs_spec.rb @@ -0,0 +1,97 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ::Gitlab::Ci::Pipeline::Chain::Limit::ActiveJobs do + let_it_be(:namespace) { create(:namespace) } + let_it_be(:project) { create(:project, namespace: namespace) } + let_it_be(:user) { create(:user) } + let_it_be(:default_plan) { create(:default_plan) } + + let(:command) do + instance_double( + ::Gitlab::Ci::Pipeline::Chain::Command, + project: project, + current_user: user, + save_incompleted: true, + pipeline_seed: pipeline_seed_double + ) + end + + let(:pipeline_seed_double) do + instance_double(::Gitlab::Ci::Pipeline::Seed::Pipeline, size: 5) + end + + let(:pipeline) do + create(:ci_pipeline, project: project) + end + + let(:existing_pipeline) { create(:ci_pipeline, project: project) } + let(:step) { described_class.new(pipeline, command) } + let(:limit) { 10 } + + subject { step.perform! } + + before do + create(:plan_limits, plan: default_plan, ci_active_jobs: limit) + namespace.clear_memoization(:actual_plan) + end + + shared_examples 'successful step' do + it 'doest not fail the pipeline and does not interrupt the chain' do + subject + + expect(pipeline).not_to be_failed + expect(step).not_to be_break + end + end + + context 'when active jobs limit is exceeded' do + before do + create_list(:ci_build, 3, pipeline: existing_pipeline) + create_list(:ci_bridge, 3, pipeline: existing_pipeline) + end + + it 'fails the pipeline with an error', :aggregate_failures do + subject + + expect(pipeline).to be_failed + expect(pipeline).to be_job_activity_limit_exceeded + expect(pipeline.errors.full_messages).to include(described_class::MESSAGE) + end + + it 'logs the failure' do + allow(Gitlab::AppLogger).to receive(:info) + + subject + + expect(Gitlab::AppLogger).to have_received(:info).with( + class: described_class.name, + message: described_class::MESSAGE, + project_id: project.id, + plan: default_plan.name + ) + end + + it 'breaks the chain' do + subject + + expect(step).to be_break + end + + context 'when active jobs limit not enabled' do + let(:limit) { 0 } + + it_behaves_like 'successful step' + end + end + + context 'when active jobs limit is not exceeded' do + before do + create_list(:ci_build, 3, pipeline: existing_pipeline) + create_list(:ci_bridge, 1, pipeline: existing_pipeline) + end + + it_behaves_like 'successful step' + end +end diff --git a/spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb index 62de4d2e96d..51d1661b586 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/populate_spec.rb @@ -236,4 +236,47 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Populate do end end end + + context 'with pipeline name' do + let(:config) do + { workflow: { name: ' Pipeline name ' }, rspec: { script: 'rspec' } } + end + + context 'with feature flag disabled' do + before do + stub_feature_flags(pipeline_name: false) + end + + it 'does not build pipeline_metadata' do + run_chain + + expect(pipeline.pipeline_metadata).to be_nil + end + end + + context 'with feature flag enabled' do + before do + stub_feature_flags(pipeline_name: true) + end + + it 'builds pipeline_metadata' do + run_chain + + expect(pipeline.pipeline_metadata.title).to eq('Pipeline name') + expect(pipeline.pipeline_metadata.project).to eq(pipeline.project) + end + + context 'with empty name' do + let(:config) do + { workflow: { name: ' ' }, rspec: { script: 'rspec' } } + end + + it 'strips whitespace from name' do + run_chain + + expect(pipeline.pipeline_metadata).to be_nil + end + end + end + end end diff --git a/spec/lib/gitlab/ci/pipeline/chain/seed_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/seed_spec.rb index 8c4f7af0ef4..323bab89e6a 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/seed_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/seed_spec.rb @@ -68,8 +68,10 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Seed do end context 'when refs policy is specified' do + let(:tag_name) { project.repository.tags.first.name } + let(:pipeline) do - build(:ci_pipeline, project: project, ref: 'feature', tag: true) + build(:ci_pipeline, project: project, ref: tag_name, tag: true) end let(:config) do diff --git a/spec/lib/gitlab/ci/pipeline/chain/sequence_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/sequence_spec.rb index ee32661f267..c69aa661b05 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/sequence_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/sequence_spec.rb @@ -100,19 +100,6 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Sequence do expect(histogram).to have_received(:observe) .with(hash_including(plan: project.actual_plan_name), 4) end - - context 'when feature flag ci_limit_active_jobs_early is disabled' do - before do - stub_feature_flags(ci_limit_active_jobs_early: false) - end - - it 'counts all the active builds' do - subject.build! - - expect(histogram).to have_received(:observe) - .with(hash_including(plan: project.actual_plan_name), 3) - end - end end end end diff --git a/spec/lib/gitlab/ci/pipeline/duration_spec.rb b/spec/lib/gitlab/ci/pipeline/duration_spec.rb index 46c7072ad8e..36714413da6 100644 --- a/spec/lib/gitlab/ci/pipeline/duration_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/duration_spec.rb @@ -1,117 +1,187 @@ # frozen_string_literal: true -require 'fast_spec_helper' +require 'spec_helper' RSpec.describe Gitlab::Ci::Pipeline::Duration do - let(:calculated_duration) { calculate(data) } + describe '.from_periods' do + let(:calculated_duration) { calculate(data) } - shared_examples 'calculating duration' do - it do - expect(calculated_duration).to eq(duration) + shared_examples 'calculating duration' do + it do + expect(calculated_duration).to eq(duration) + end end - end - context 'test sample A' do - let(:data) do - [[0, 1], - [1, 2], - [3, 4], - [5, 6]] + context 'test sample A' do + let(:data) do + [[0, 1], + [1, 2], + [3, 4], + [5, 6]] + end + + let(:duration) { 4 } + + it_behaves_like 'calculating duration' end - let(:duration) { 4 } + context 'test sample B' do + let(:data) do + [[0, 1], + [1, 2], + [2, 3], + [3, 4], + [0, 4]] + end - it_behaves_like 'calculating duration' - end + let(:duration) { 4 } - context 'test sample B' do - let(:data) do - [[0, 1], - [1, 2], - [2, 3], - [3, 4], - [0, 4]] + it_behaves_like 'calculating duration' end - let(:duration) { 4 } + context 'test sample C' do + let(:data) do + [[0, 4], + [2, 6], + [5, 7], + [8, 9]] + end - it_behaves_like 'calculating duration' - end + let(:duration) { 8 } - context 'test sample C' do - let(:data) do - [[0, 4], - [2, 6], - [5, 7], - [8, 9]] + it_behaves_like 'calculating duration' end - let(:duration) { 8 } + context 'test sample D' do + let(:data) do + [[0, 1], + [2, 3], + [4, 5], + [6, 7]] + end - it_behaves_like 'calculating duration' - end + let(:duration) { 4 } + + it_behaves_like 'calculating duration' + end - context 'test sample D' do - let(:data) do - [[0, 1], - [2, 3], - [4, 5], - [6, 7]] + context 'test sample E' do + let(:data) do + [[0, 1], + [3, 9], + [3, 4], + [3, 5], + [3, 8], + [4, 5], + [4, 7], + [5, 8]] + end + + let(:duration) { 7 } + + it_behaves_like 'calculating duration' end - let(:duration) { 4 } + context 'test sample F' do + let(:data) do + [[1, 3], + [2, 4], + [2, 4], + [2, 4], + [5, 8]] + end - it_behaves_like 'calculating duration' - end + let(:duration) { 6 } - context 'test sample E' do - let(:data) do - [[0, 1], - [3, 9], - [3, 4], - [3, 5], - [3, 8], - [4, 5], - [4, 7], - [5, 8]] + it_behaves_like 'calculating duration' end - let(:duration) { 7 } + context 'test sample G' do + let(:data) do + [[1, 3], + [2, 4], + [6, 7]] + end - it_behaves_like 'calculating duration' - end + let(:duration) { 4 } - context 'test sample F' do - let(:data) do - [[1, 3], - [2, 4], - [2, 4], - [2, 4], - [5, 8]] + it_behaves_like 'calculating duration' end - let(:duration) { 6 } + def calculate(data) + periods = data.shuffle.map do |(first, last)| + described_class::Period.new(first, last) + end - it_behaves_like 'calculating duration' + described_class.from_periods(periods.sort_by(&:first)) + end end - context 'test sample G' do - let(:data) do - [[1, 3], - [2, 4], - [6, 7]] + describe '.from_pipeline' do + let_it_be(:start_time) { Time.current.change(usec: 0) } + let_it_be(:current) { start_time + 1000 } + let_it_be(:pipeline) { create(:ci_pipeline) } + let_it_be(:success_build) { create_build(:success, started_at: start_time, finished_at: start_time + 60) } + let_it_be(:failed_build) { create_build(:failed, started_at: start_time + 60, finished_at: start_time + 120) } + let_it_be(:canceled_build) { create_build(:canceled, started_at: start_time + 120, finished_at: start_time + 180) } + let_it_be(:skipped_build) { create_build(:skipped, started_at: start_time) } + let_it_be(:pending_build) { create_build(:pending) } + let_it_be(:created_build) { create_build(:created) } + let_it_be(:preparing_build) { create_build(:preparing) } + let_it_be(:scheduled_build) { create_build(:scheduled) } + let_it_be(:expired_scheduled_build) { create_build(:expired_scheduled) } + let_it_be(:manual_build) { create_build(:manual) } + + let!(:running_build) { create_build(:running, started_at: start_time) } + + it 'returns the duration of the running build' do + travel_to(current) do + expect(described_class.from_pipeline(pipeline)).to eq 1000.seconds + end end - let(:duration) { 4 } + context 'when there is no running build' do + let(:running_build) { nil } - it_behaves_like 'calculating duration' - end + it 'returns the duration for all the builds' do + travel_to(current) do + expect(described_class.from_pipeline(pipeline)).to eq 180.seconds + end + end + end - def calculate(data) - periods = data.shuffle.map do |(first, last)| - described_class::Period.new(first, last) + context 'when there are bridge jobs' do + let!(:success_bridge) { create_bridge(:success, started_at: start_time + 220, finished_at: start_time + 280) } + let!(:failed_bridge) { create_bridge(:failed, started_at: start_time + 180, finished_at: start_time + 240) } + let!(:skipped_bridge) { create_bridge(:skipped, started_at: start_time) } + let!(:created_bridge) { create_bridge(:created) } + let!(:manual_bridge) { create_bridge(:manual) } + + it 'returns the duration of the running build' do + travel_to(current) do + expect(described_class.from_pipeline(pipeline)).to eq 1000.seconds + end + end + + context 'when there is no running build' do + let!(:running_build) { nil } + + it 'returns the duration for all the builds and bridge jobs' do + travel_to(current) do + expect(described_class.from_pipeline(pipeline)).to eq 280.seconds + end + end + end end - described_class.from_periods(periods.sort_by(&:first)) + private + + def create_build(trait, **opts) + create(:ci_build, trait, pipeline: pipeline, **opts) + end + + def create_bridge(trait, **opts) + create(:ci_bridge, trait, pipeline: pipeline, **opts) + end end end diff --git a/spec/lib/gitlab/ci/pipeline/logger_spec.rb b/spec/lib/gitlab/ci/pipeline/logger_spec.rb index f31361431f2..3af0ebe7484 100644 --- a/spec/lib/gitlab/ci/pipeline/logger_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/logger_spec.rb @@ -25,6 +25,7 @@ RSpec.describe ::Gitlab::Ci::Pipeline::Logger do loggable_data = { 'expensive_operation_duration_s' => { 'count' => 1, + 'sum' => a_kind_of(Numeric), 'avg' => a_kind_of(Numeric), 'max' => a_kind_of(Numeric), 'min' => a_kind_of(Numeric) @@ -62,6 +63,7 @@ RSpec.describe ::Gitlab::Ci::Pipeline::Logger do accumulator[key] = { 'count' => count, 'avg' => a_kind_of(Numeric), + 'sum' => a_kind_of(Numeric), 'max' => a_kind_of(Numeric), 'min' => a_kind_of(Numeric) } @@ -71,6 +73,7 @@ RSpec.describe ::Gitlab::Ci::Pipeline::Logger do data['expensive_operation_db_count']['max'] = db_count data['expensive_operation_db_count']['min'] = db_count data['expensive_operation_db_count']['avg'] = db_count + data['expensive_operation_db_count']['sum'] = count * db_count end data @@ -131,7 +134,7 @@ RSpec.describe ::Gitlab::Ci::Pipeline::Logger do it 'records durations of observed operations' do loggable_data = { 'pipeline_creation_duration_s' => { - 'avg' => 30, 'count' => 1, 'max' => 30, 'min' => 30 + 'avg' => 30, 'sum' => 30, 'count' => 1, 'max' => 30, 'min' => 30 } } @@ -165,10 +168,10 @@ RSpec.describe ::Gitlab::Ci::Pipeline::Logger do 'pipeline_creation_caller' => 'source', 'pipeline_source' => pipeline.source, 'pipeline_save_duration_s' => { - 'avg' => 60, 'count' => 1, 'max' => 60, 'min' => 60 + 'avg' => 60, 'sum' => 60, 'count' => 1, 'max' => 60, 'min' => 60 }, 'pipeline_creation_duration_s' => { - 'avg' => 20, 'count' => 2, 'max' => 30, 'min' => 10 + 'avg' => 20, 'sum' => 40, 'count' => 2, 'max' => 30, 'min' => 10 } } end @@ -215,10 +218,10 @@ RSpec.describe ::Gitlab::Ci::Pipeline::Logger do 'pipeline_creation_service_duration_s' => a_kind_of(Numeric), 'pipeline_creation_caller' => 'source', 'pipeline_save_duration_s' => { - 'avg' => 60, 'count' => 1, 'max' => 60, 'min' => 60 + 'avg' => 60, 'sum' => 60, 'count' => 1, 'max' => 60, 'min' => 60 }, 'pipeline_creation_duration_s' => { - 'avg' => 20, 'count' => 2, 'max' => 30, 'min' => 10 + 'avg' => 20, 'sum' => 40, 'count' => 2, 'max' => 30, 'min' => 10 } } end diff --git a/spec/lib/gitlab/ci/reports/codequality_reports_comparer_spec.rb b/spec/lib/gitlab/ci/reports/codequality_reports_comparer_spec.rb index e289e59b281..effa2c43418 100644 --- a/spec/lib/gitlab/ci/reports/codequality_reports_comparer_spec.rb +++ b/spec/lib/gitlab/ci/reports/codequality_reports_comparer_spec.rb @@ -191,11 +191,12 @@ RSpec.describe Gitlab::Ci::Reports::CodequalityReportsComparer do end it 'includes the base report errors sorted by severity' do - expect(existing_errors).to eq([ - blocker_degradation, - critical_degradation, - major_degradation - ]) + expect(existing_errors).to eq( + [ + blocker_degradation, + critical_degradation, + major_degradation + ]) end end @@ -242,11 +243,12 @@ RSpec.describe Gitlab::Ci::Reports::CodequalityReportsComparer do end it 'includes errors not found in the base report sorted by severity' do - expect(new_errors).to eq([ - blocker_degradation, - critical_degradation, - minor_degradation - ]) + expect(new_errors).to eq( + [ + blocker_degradation, + critical_degradation, + minor_degradation + ]) end end @@ -304,11 +306,12 @@ RSpec.describe Gitlab::Ci::Reports::CodequalityReportsComparer do end it 'returns the base report errors not found in the head report, sorted by severity' do - expect(resolved_errors).to eq([ - blocker_degradation, - critical_degradation, - minor_degradation - ]) + expect(resolved_errors).to eq( + [ + blocker_degradation, + critical_degradation, + minor_degradation + ]) end end diff --git a/spec/lib/gitlab/ci/reports/codequality_reports_spec.rb b/spec/lib/gitlab/ci/reports/codequality_reports_spec.rb index f4b47893805..68e70525c55 100644 --- a/spec/lib/gitlab/ci/reports/codequality_reports_spec.rb +++ b/spec/lib/gitlab/ci/reports/codequality_reports_spec.rb @@ -103,15 +103,16 @@ RSpec.describe Gitlab::Ci::Reports::CodequalityReports do end it 'sorts degradations based on severity' do - expect(codequality_report.degradations.values).to eq([ - blocker, - critical, - major, - major_2, - minor, - info, - unknown - ]) + expect(codequality_report.degradations.values).to eq( + [ + blocker, + critical, + major, + major_2, + minor, + info, + unknown + ]) end context 'with non-existence and uppercase severities' do @@ -126,12 +127,13 @@ RSpec.describe Gitlab::Ci::Reports::CodequalityReports do end it 'sorts unknown last' do - expect(other_report.degradations.values).to eq([ - blocker, - uppercase_major, - minor, - non_existent - ]) + expect(other_report.degradations.values).to eq( + [ + blocker, + uppercase_major, + minor, + non_existent + ]) end end end diff --git a/spec/lib/gitlab/ci/reports/sbom/source_spec.rb b/spec/lib/gitlab/ci/reports/sbom/source_spec.rb index cb30bd721dd..343c0d8c15c 100644 --- a/spec/lib/gitlab/ci/reports/sbom/source_spec.rb +++ b/spec/lib/gitlab/ci/reports/sbom/source_spec.rb @@ -12,8 +12,7 @@ RSpec.describe Gitlab::Ci::Reports::Sbom::Source do 'source_file' => { 'path' => 'package.json' }, 'package_manager' => { 'name' => 'npm' }, 'language' => { 'name' => 'JavaScript' } - }, - fingerprint: '4dbcb747e6f0fb3ed4f48d96b777f1d64acdf43e459fdfefad404e55c004a188' + } } end @@ -22,8 +21,7 @@ RSpec.describe Gitlab::Ci::Reports::Sbom::Source do it 'has correct attributes' do expect(subject).to have_attributes( source_type: attributes[:type], - data: attributes[:data], - fingerprint: attributes[:fingerprint] + data: attributes[:data] ) end end diff --git a/spec/lib/gitlab/ci/reports/security/report_spec.rb b/spec/lib/gitlab/ci/reports/security/report_spec.rb index ab0efb90901..d7f967f1c55 100644 --- a/spec/lib/gitlab/ci/reports/security/report_spec.rb +++ b/spec/lib/gitlab/ci/reports/security/report_spec.rb @@ -140,6 +140,24 @@ RSpec.describe Gitlab::Ci::Reports::Security::Report do it { is_expected.to eq(scanner_1) } end + describe '#primary_identifiers' do + it 'returns matching identifiers' do + scanner_with_identifiers = create( + :ci_reports_security_scanner, + external_id: 'external_id_1', + primary_identifiers: [create(:ci_reports_security_identifier, external_id: 'other_id', name: 'other_scanner')] + ) + scanner_without_identifiers = create( + :ci_reports_security_scanner, + external_id: 'external_id_2') + + report.add_scanner(scanner_with_identifiers) + report.add_scanner(scanner_without_identifiers) + + expect(report.primary_identifiers).to eq(scanner_with_identifiers.primary_identifiers) + end + 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 diff --git a/spec/lib/gitlab/ci/secure_files/cer_spec.rb b/spec/lib/gitlab/ci/secure_files/cer_spec.rb new file mode 100644 index 00000000000..6b9cd0e3bfc --- /dev/null +++ b/spec/lib/gitlab/ci/secure_files/cer_spec.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::SecureFiles::Cer do + context 'when the supplied certificate cannot be parsed' do + let(:invalid_certificate) { described_class.new('xyzabc') } + + describe '#certificate_data' do + it 'assigns the error message and returns nil' do + expect(invalid_certificate.certificate_data).to be nil + expect(invalid_certificate.error).to eq('not enough data') + end + end + + describe '#metadata' do + it 'returns an empty hash' do + expect(invalid_certificate.metadata).to eq({}) + end + end + + describe '#expires_at' do + it 'returns nil' do + expect(invalid_certificate.metadata[:expires_at]).to be_nil + end + end + end + + context 'when the supplied certificate can be parsed' do + let(:sample_file) { fixture_file('ci_secure_files/sample.cer') } + let(:subject) { described_class.new(sample_file) } + + describe '#certificate_data' do + it 'returns an OpenSSL::X509::Certificate object' do + expect(subject.certificate_data.class).to be(OpenSSL::X509::Certificate) + end + end + + describe '#metadata' do + it 'returns a hash with the expected keys' do + expect(subject.metadata.keys).to match_array([:issuer, :subject, :id, :expires_at]) + end + end + + describe '#id' do + it 'returns the certificate serial number' do + expect(subject.metadata[:id]).to eq('33669367788748363528491290218354043267') + end + end + + describe '#expires_at' do + it 'returns the certificate expiration timestamp' do + expect(subject.metadata[:expires_at]).to eq('2022-04-26 19:20:40 UTC') + end + end + + describe '#issuer' do + it 'calls parse on X509Name' do + expect(subject.metadata[:issuer]["O"]).to eq('Apple Inc.') + end + end + + describe '#subject' do + it 'calls parse on X509Name' do + expect(subject.metadata[:subject]["OU"]).to eq('N7SYAN8PX8') + end + end + end +end diff --git a/spec/lib/gitlab/ci/secure_files/mobile_provision_spec.rb b/spec/lib/gitlab/ci/secure_files/mobile_provision_spec.rb new file mode 100644 index 00000000000..fb382174c64 --- /dev/null +++ b/spec/lib/gitlab/ci/secure_files/mobile_provision_spec.rb @@ -0,0 +1,149 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::SecureFiles::MobileProvision do + context 'when the supplied profile cannot be parsed' do + context 'when the supplied certificate cannot be parsed' do + let(:invalid_profile) { described_class.new('xyzabc') } + + describe '#decoded_plist' do + it 'assigns the error message and returns nil' do + expect(invalid_profile.decoded_plist).to be nil + expect(invalid_profile.error).to eq('Could not parse the PKCS7: not enough data') + end + end + + describe '#properties' do + it 'returns nil' do + expect(invalid_profile.properties).to be_nil + end + end + + describe '#metadata' do + it 'returns an empty hash' do + expect(invalid_profile.metadata).to eq({}) + end + end + + describe '#expires_at' do + it 'returns nil' do + expect(invalid_profile.metadata[:expires_at]).to be_nil + end + end + end + end + + context 'when the supplied profile can be parsed' do + let(:sample_file) { fixture_file('ci_secure_files/sample.mobileprovision') } + let(:subject) { described_class.new(sample_file) } + + describe '#decoded_plist' do + it 'returns an XML string' do + expect(subject.decoded_plist.class).to be(String) + expect(subject.decoded_plist.starts_with?('<?xml version="1.0"')).to be true + end + end + + describe '#properties' do + it 'returns the property list of the decoded plist provided' do + expect(subject.properties.class).to be(Hash) + expect(subject.properties.keys).to match_array(%w[AppIDName ApplicationIdentifierPrefix CreationDate + Platform IsXcodeManaged DeveloperCertificates + DER-Encoded-Profile PPQCheck Entitlements ExpirationDate + Name ProvisionedDevices TeamIdentifier TeamName + TimeToLive UUID Version]) + end + + it 'returns nil if the property list fails to be parsed from the decoded plist' do + allow(subject).to receive(:decoded_plist).and_return('foo/bar') + expect(subject.properties).to be nil + expect(subject.error).to start_with('invalid XML') + end + end + + describe '#metadata' do + it 'returns a hash with the expected keys' do + expect(subject.metadata.keys).to match_array([:id, :expires_at, :app_id, :app_id_prefix, :app_name, + :certificate_ids, :devices, :entitlements, :platforms, + :team_id, :team_name, :xcode_managed]) + end + end + + describe '#id' do + it 'returns the profile UUID' do + expect(subject.metadata[:id]).to eq('6b9fcce1-b9a9-4b37-b2ce-ec4da2044abf') + end + end + + describe '#expires_at' do + it 'returns the expiration timestamp of the profile' do + expect(subject.metadata[:expires_at].utc).to eq('2023-08-01 23:15:13 UTC') + end + end + + describe '#platforms' do + it 'returns the platforms assigned to the profile' do + expect(subject.metadata[:platforms]).to match_array(['iOS']) + end + end + + describe '#team_name' do + it 'returns the team name in the profile' do + expect(subject.metadata[:team_name]).to eq('Darby Frey') + end + end + + describe '#team_id' do + it 'returns the team ids in the profile' do + expect(subject.metadata[:team_id]).to match_array(['N7SYAN8PX8']) + end + end + + describe '#app_name' do + it 'returns the app name in the profile' do + expect(subject.metadata[:app_name]).to eq('iOS Demo') + end + end + + describe '#app_id' do + it 'returns the app id in the profile' do + expect(subject.metadata[:app_id]).to eq('match Development com.gitlab.ios-demo') + end + end + + describe '#app_id_prefix' do + it 'returns the app id prefixes in the profile' do + expect(subject.metadata[:app_id_prefix]).to match_array(['N7SYAN8PX8']) + end + end + + describe '#xcode_managed' do + it 'returns the xcode_managed property in the profile' do + expect(subject.metadata[:xcode_managed]).to be false + end + end + + describe '#entitlements' do + it 'returns the entitlements in the profile' do + expect(subject.metadata[:entitlements].keys).to match_array(['application-identifier', + 'com.apple.developer.game-center', + 'com.apple.developer.team-identifier', + 'get-task-allow', + 'keychain-access-groups']) + end + end + + describe '#devices' do + it 'returns the devices attached to the profile' do + expect(subject.metadata[:devices]).to match_array(["00008101-001454860C10001E"]) + end + end + + describe '#certificate_ids' do + it 'returns the certificate ids attached to the profile' do + expect(subject.metadata[:certificate_ids]).to match_array(["23380136242930206312716563638445789376"]) + end + end + end +end diff --git a/spec/lib/gitlab/ci/secure_files/p12_spec.rb b/spec/lib/gitlab/ci/secure_files/p12_spec.rb new file mode 100644 index 00000000000..beabf4b4856 --- /dev/null +++ b/spec/lib/gitlab/ci/secure_files/p12_spec.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::SecureFiles::P12 do + context 'when the supplied certificate cannot be parsed' do + let(:invalid_certificate) { described_class.new('xyzabc') } + + describe '#certificate_data' do + it 'assigns the error message and returns nil' do + expect(invalid_certificate.certificate_data).to be nil + expect(invalid_certificate.error).to eq('PKCS12_parse: mac verify failure') + end + end + + describe '#metadata' do + it 'returns an empty hash' do + expect(invalid_certificate.metadata).to eq({}) + end + end + + describe '#expires_at' do + it 'returns nil' do + expect(invalid_certificate.metadata[:expires_at]).to be_nil + end + end + end + + context 'when the supplied certificate can be parsed, but the password is invalid' do + let(:sample_file) { fixture_file('ci_secure_files/sample.p12') } + let(:subject) { described_class.new(sample_file, 'foo') } + + describe '#certificate_data' do + it 'assigns the error message and returns nil' do + expect(subject.certificate_data).to be nil + expect(subject.error).to eq('PKCS12_parse: mac verify failure') + end + end + end + + context 'when the supplied certificate can be parsed' do + let(:sample_file) { fixture_file('ci_secure_files/sample.p12') } + let(:subject) { described_class.new(sample_file) } + + describe '#certificate_data' do + it 'returns an OpenSSL::X509::Certificate object' do + expect(subject.certificate_data.class).to be(OpenSSL::X509::Certificate) + end + end + + describe '#metadata' do + it 'returns a hash with the expected keys' do + expect(subject.metadata.keys).to match_array([:issuer, :subject, :id, :expires_at]) + end + end + + describe '#id' do + it 'returns the certificate serial number' do + expect(subject.metadata[:id]).to eq('75949910542696343243264405377658443914') + end + end + + describe '#expires_at' do + it 'returns the certificate expiration timestamp' do + expect(subject.metadata[:expires_at]).to eq('2022-09-21 14:56:00 UTC') + end + end + + describe '#issuer' do + it 'calls parse on X509Name' do + expect(subject.metadata[:issuer]["O"]).to eq('Apple Inc.') + end + end + + describe '#subject' do + it 'calls parse on X509Name' do + expect(subject.metadata[:subject]["OU"]).to eq('N7SYAN8PX8') + end + end + end +end diff --git a/spec/lib/gitlab/ci/secure_files/x509_name_spec.rb b/spec/lib/gitlab/ci/secure_files/x509_name_spec.rb new file mode 100644 index 00000000000..3a523924c5b --- /dev/null +++ b/spec/lib/gitlab/ci/secure_files/x509_name_spec.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::SecureFiles::X509Name do + describe '.parse' do + it 'parses an X509Name object into a hash format' do + sample = OpenSSL::X509::Name.new([ + ['C', 'Test Country'], + ['O', 'Test Org Name'], + ['OU', 'Test Org Unit'], + ['CN', 'Test Common Name'], + ['UID', 'Test UID'] + ]) + + parsed_sample = described_class.parse(sample) + + expect(parsed_sample["C"]).to eq('Test Country') + expect(parsed_sample["O"]).to eq('Test Org Name') + expect(parsed_sample["OU"]).to eq('Test Org Unit') + expect(parsed_sample["CN"]).to eq('Test Common Name') + expect(parsed_sample["UID"]).to eq('Test UID') + end + + it 'returns an empty hash when an error occurs' do + parsed_sample = described_class.parse('unexpectedinput') + expect(parsed_sample).to eq({}) + end + end +end diff --git a/spec/lib/gitlab/ci/trace_spec.rb b/spec/lib/gitlab/ci/trace_spec.rb index 3043c8c5467..321a47c0634 100644 --- a/spec/lib/gitlab/ci/trace_spec.rb +++ b/spec/lib/gitlab/ci/trace_spec.rb @@ -74,7 +74,7 @@ RSpec.describe Gitlab::Ci::Trace, :clean_gitlab_redis_shared_state, factory_defa trace.being_watched! result = Gitlab::Redis::SharedState.with do |redis| - redis.exists(cache_key) + redis.exists?(cache_key) end expect(result).to eq(true) diff --git a/spec/lib/gitlab/ci/variables/builder/group_spec.rb b/spec/lib/gitlab/ci/variables/builder/group_spec.rb index 72487588cde..c3743ebd2d7 100644 --- a/spec/lib/gitlab/ci/variables/builder/group_spec.rb +++ b/spec/lib/gitlab/ci/variables/builder/group_spec.rb @@ -132,11 +132,12 @@ RSpec.describe Gitlab::Ci::Variables::Builder::Group do end it 'orders the variables from least to most matched' do - variables_collection = Gitlab::Ci::Variables::Collection.new([ - variable, - partially_matched_variable, - perfectly_matched_variable - ]).to_runner_variables + variables_collection = Gitlab::Ci::Variables::Collection.new( + [ + variable, + partially_matched_variable, + perfectly_matched_variable + ]).to_runner_variables expect(subject.to_runner_variables).to eq(variables_collection) end diff --git a/spec/lib/gitlab/ci/variables/builder/project_spec.rb b/spec/lib/gitlab/ci/variables/builder/project_spec.rb index b64b6ea98e2..c1cefc425f5 100644 --- a/spec/lib/gitlab/ci/variables/builder/project_spec.rb +++ b/spec/lib/gitlab/ci/variables/builder/project_spec.rb @@ -132,11 +132,12 @@ RSpec.describe Gitlab::Ci::Variables::Builder::Project do end it 'puts variables matching environment scope more in the end' do - variables_collection = Gitlab::Ci::Variables::Collection.new([ - variable, - partially_matched_variable, - perfectly_matched_variable - ]).to_runner_variables + variables_collection = Gitlab::Ci::Variables::Collection.new( + [ + variable, + partially_matched_variable, + perfectly_matched_variable + ]).to_runner_variables expect(subject.to_runner_variables).to eq(variables_collection) end diff --git a/spec/lib/gitlab/ci/variables/builder/release_spec.rb b/spec/lib/gitlab/ci/variables/builder/release_spec.rb new file mode 100644 index 00000000000..85b1659d07b --- /dev/null +++ b/spec/lib/gitlab/ci/variables/builder/release_spec.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Variables::Builder::Release do + let_it_be(:project) { create(:project, :public, :repository) } + let_it_be(:release) { create(:release, project: project) } + + let(:builder) { described_class.new(release) } + + describe '#variables' do + let(:description_variable) do + { + key: 'CI_RELEASE_DESCRIPTION', + value: release.description, + public: true, + masked: false, + raw: true + } + end + + subject do + builder.variables + end + + context 'when the release is present' do + let(:description_item) { item(description_variable) } + + it 'contains all the variables' do + is_expected.to contain_exactly(description_item) + end + + context 'for large description' do + before do + release.update_attribute(:description, "Test Description ..." * 5000) + end + + it 'truncates' do + expect(subject['CI_RELEASE_DESCRIPTION'].value.length).to eq(1024) + end + end + + context 'when description is nil' do + before do + release.update_attribute(:description, nil) + end + + it 'returns without error' do + builder = subject + + expect(builder).to match_array([]) + expect(builder.errors).to be_nil + end + end + end + + context 'when the release is not present' do + let(:release) { nil } + + it 'contains no variables' do + is_expected.to match_array([]) + end + end + end + + def item(variable) + ::Gitlab::Ci::Variables::Collection::Item.fabricate(variable) + end +end diff --git a/spec/lib/gitlab/ci/variables/builder_spec.rb b/spec/lib/gitlab/ci/variables/builder_spec.rb index 4833ccf9093..52ba85d2df1 100644 --- a/spec/lib/gitlab/ci/variables/builder_spec.rb +++ b/spec/lib/gitlab/ci/variables/builder_spec.rb @@ -10,6 +10,7 @@ RSpec.describe Gitlab::Ci::Variables::Builder, :clean_gitlab_redis_cache do let_it_be(:user) { create(:user) } let_it_be_with_reload(:job) do create(:ci_build, + name: 'rspec:test 1', pipeline: pipeline, user: user, yaml_variables: [{ key: 'YAML_VARIABLE', value: 'value' }] @@ -24,13 +25,15 @@ RSpec.describe Gitlab::Ci::Variables::Builder, :clean_gitlab_redis_cache do let(:predefined_variables) do [ { key: 'CI_JOB_NAME', - value: job.name }, + value: 'rspec:test 1' }, + { key: 'CI_JOB_NAME_SLUG', + value: 'rspec-test-1' }, { key: 'CI_JOB_STAGE', value: job.stage_name }, { key: 'CI_NODE_TOTAL', value: '1' }, { key: 'CI_BUILD_NAME', - value: job.name }, + value: 'rspec:test 1' }, { key: 'CI_BUILD_STAGE', value: job.stage_name }, { key: 'CI', @@ -171,6 +174,7 @@ RSpec.describe Gitlab::Ci::Variables::Builder, :clean_gitlab_redis_cache do allow(builder).to receive(:secret_project_variables) { [var('L', 12), var('M', 12)] } allow(pipeline).to receive(:variables) { [var('M', 13), var('N', 13)] } allow(pipeline).to receive(:pipeline_schedule) { double(job_variables: [var('N', 14), var('O', 14)]) } + allow(builder).to receive(:release_variables) { [var('P', 15), var('Q', 15)] } end it 'returns variables in order depending on resource hierarchy' do @@ -187,7 +191,8 @@ RSpec.describe Gitlab::Ci::Variables::Builder, :clean_gitlab_redis_cache do var('K', 11), var('L', 11), var('L', 12), var('M', 12), var('M', 13), var('N', 13), - var('N', 14), var('O', 14)]) + var('N', 14), var('O', 14), + var('P', 15), var('Q', 15)]) end it 'overrides duplicate keys depending on resource hierarchy' do @@ -199,7 +204,8 @@ RSpec.describe Gitlab::Ci::Variables::Builder, :clean_gitlab_redis_cache do 'I' => '9', 'J' => '10', 'K' => '11', 'L' => '12', 'M' => '13', 'N' => '14', - 'O' => '14') + 'O' => '14', 'P' => '15', + 'Q' => '15') end end @@ -216,6 +222,27 @@ RSpec.describe Gitlab::Ci::Variables::Builder, :clean_gitlab_redis_cache do .to include(a_hash_including(key: schedule_variable.key, value: schedule_variable.value)) end end + + context 'with release variables' do + let(:release_description_key) { 'CI_RELEASE_DESCRIPTION' } + + let_it_be(:tag) { project.repository.tags.first } + let_it_be(:pipeline) { create(:ci_pipeline, project: project, tag: true, ref: tag.name) } + let_it_be(:release) { create(:release, tag: tag.name, project: project) } + + it 'includes release variables' do + expect(subject.to_hash).to include(release_description_key => release.description) + end + + context 'when there is no release' do + let_it_be(:pipeline) { create(:ci_pipeline, project: project, tag: false, ref: 'master') } + let(:release) { nil } + + it 'does not include release variables' do + expect(subject.to_hash).not_to have_key(release_description_key) + end + end + end end describe '#user_variables' do @@ -261,10 +288,11 @@ RSpec.describe Gitlab::Ci::Variables::Builder, :clean_gitlab_redis_cache do end it 'includes #deployment_variables and merges the KUBECONFIG values', :aggregate_failures do - expect(builder).to receive(:deployment_variables).and_return([ - { key: 'KUBECONFIG', value: 'deployment-kubeconfig' }, - { key: 'OTHER', value: 'some value' } - ]) + expect(builder).to receive(:deployment_variables).and_return( + [ + { key: 'KUBECONFIG', value: 'deployment-kubeconfig' }, + { key: 'OTHER', value: 'some value' } + ]) expect(template).to receive(:merge_yaml).with('deployment-kubeconfig') expect(subject['KUBECONFIG'].value).to eq('example-kubeconfig') expect(subject['OTHER'].value).to eq('some value') diff --git a/spec/lib/gitlab/ci/variables/collection/sort_spec.rb b/spec/lib/gitlab/ci/variables/collection/sort_spec.rb index 57171e5be69..432225c53f0 100644 --- a/spec/lib/gitlab/ci/variables/collection/sort_spec.rb +++ b/spec/lib/gitlab/ci/variables/collection/sort_spec.rb @@ -192,13 +192,14 @@ RSpec.describe Gitlab::Ci::Variables::Collection::Sort do end it 'preserves relative order of overridden variables' do - is_expected.to eq([ - { 'TOP_LEVEL_GROUP_NAME' => 'top-level-group' }, - { 'SUBGROUP_VAR' => '$TOP_LEVEL_GROUP_NAME' }, - { 'SUB_GROUP_NAME' => 'vars-in-vars-subgroup' }, - { 'SUBGROUP_VAR' => '$SUB_GROUP_NAME' }, - { 'PROJECT_VAR' => '$SUBGROUP_VAR' } - ]) + is_expected.to eq( + [ + { 'TOP_LEVEL_GROUP_NAME' => 'top-level-group' }, + { 'SUBGROUP_VAR' => '$TOP_LEVEL_GROUP_NAME' }, + { 'SUB_GROUP_NAME' => 'vars-in-vars-subgroup' }, + { 'SUBGROUP_VAR' => '$SUB_GROUP_NAME' }, + { 'PROJECT_VAR' => '$SUBGROUP_VAR' } + ]) end end end diff --git a/spec/lib/gitlab/ci/variables/collection_spec.rb b/spec/lib/gitlab/ci/variables/collection_spec.rb index 8ac03301322..7d4a1eef70b 100644 --- a/spec/lib/gitlab/ci/variables/collection_spec.rb +++ b/spec/lib/gitlab/ci/variables/collection_spec.rb @@ -571,5 +571,42 @@ RSpec.describe Gitlab::Ci::Variables::Collection do end end end + + context 'with the file_variable_is_referenced_in_another_variable logging' do + let(:collection) do + Gitlab::Ci::Variables::Collection.new + .append(key: 'VAR1', value: 'test-1') + .append(key: 'VAR2', value: '$VAR1') + .append(key: 'VAR3', value: '$VAR1', raw: true) + .append(key: 'FILEVAR4', value: 'file-test-4', file: true) + .append(key: 'VAR5', value: '$FILEVAR4') + .append(key: 'VAR6', value: '$FILEVAR4', raw: true) + end + + subject(:sort_and_expand_all) { collection.sort_and_expand_all(project: project) } + + context 'when a project is not passed' do + let(:project) {} + + it 'does not log anything' do + expect(Gitlab::AppJsonLogger).not_to receive(:info) + + sort_and_expand_all + end + end + + context 'when a project is passed' do + let(:project) { create(:project) } + + it 'logs file_variable_is_referenced_in_another_variable once for VAR5' do + expect(Gitlab::AppJsonLogger).to receive(:info).with( + event: 'file_variable_is_referenced_in_another_variable', + project_id: project.id + ).once + + sort_and_expand_all + end + end + end end end diff --git a/spec/lib/gitlab/ci/yaml_processor/result_spec.rb b/spec/lib/gitlab/ci/yaml_processor/result_spec.rb index f7a0905d9da..7f203168706 100644 --- a/spec/lib/gitlab/ci/yaml_processor/result_spec.rb +++ b/spec/lib/gitlab/ci/yaml_processor/result_spec.rb @@ -71,10 +71,11 @@ module Gitlab subject(:yaml_variables_for) { result.yaml_variables_for(job_name) } it 'returns calculated variables with root and job variables' do - is_expected.to match_array([ - { key: 'VAR1', value: 'value 11' }, - { key: 'VAR2', value: 'value 2' } - ]) + is_expected.to match_array( + [ + { key: 'VAR1', value: 'value 11' }, + { key: 'VAR2', value: 'value 2' } + ]) end context 'when an absent job is sent' do diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb index cc327f5b5f1..ebf8422489e 100644 --- a/spec/lib/gitlab/ci/yaml_processor_spec.rb +++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb @@ -15,8 +15,10 @@ module Gitlab end end - describe '#build_attributes' do - subject { described_class.new(config, user: nil).execute.build_attributes(:rspec) } + describe '#builds' do + subject(:builds) { described_class.new(config, user: nil).execute.builds } + + let(:rspec_build) { builds.find { |build| build[:name] == 'rspec' } } describe 'attributes list' do let(:config) do @@ -30,7 +32,7 @@ module Gitlab end it 'returns valid build attributes' do - expect(subject).to eq({ + expect(builds).to eq([{ stage: "test", stage_idx: 2, name: "rspec", @@ -45,7 +47,7 @@ module Gitlab job_variables: [], root_variables_inheritance: true, scheduling_type: :stage - }) + }]) end end @@ -63,7 +65,7 @@ module Gitlab end it 'returns valid build attributes' do - expect(subject).to eq({ + expect(builds).to eq([{ stage: 'test', stage_idx: 2, name: 'rspec', @@ -77,7 +79,7 @@ module Gitlab job_variables: [], root_variables_inheritance: true, scheduling_type: :stage - }) + }]) end end @@ -89,21 +91,22 @@ module Gitlab end it 'includes coverage regexp in build attributes' do - expect(subject) + expect(rspec_build) .to include(coverage_regex: 'Code coverage: \d+\.\d+') end end end describe 'tags entry with default values' do - it 'applies default values' do - config = YAML.dump({ default: { tags: %w[A B] }, - rspec: { script: "rspec" } }) - - config_processor = Gitlab::Ci::YamlProcessor.new(config).execute + let(:config) do + YAML.dump( + default: { tags: %w[A B] }, + rspec: { script: "rspec" } + ) + end - expect(config_processor.stage_builds_attributes("test").size).to eq(1) - expect(config_processor.stage_builds_attributes("test").first).to eq({ + it 'applies default values' do + expect(rspec_build).to eq({ stage: "test", stage_idx: 2, name: "rspec", @@ -125,7 +128,7 @@ module Gitlab YAML.dump(rspec: { script: 'rspec', interruptible: true }) end - it { expect(subject[:interruptible]).to be_truthy } + it { expect(rspec_build[:interruptible]).to be_truthy } end describe 'interruptible job with default value' do @@ -133,7 +136,7 @@ module Gitlab YAML.dump(rspec: { script: 'rspec' }) end - it { expect(subject).not_to have_key(:interruptible) } + it { expect(rspec_build).not_to have_key(:interruptible) } end describe 'uninterruptible job' do @@ -141,7 +144,7 @@ module Gitlab YAML.dump(rspec: { script: 'rspec', interruptible: false }) end - it { expect(subject[:interruptible]).to be_falsy } + it { expect(rspec_build[:interruptible]).to be_falsy } end it "returns interruptible when overridden for job" do @@ -149,9 +152,10 @@ module Gitlab rspec: { script: "rspec" } }) config_processor = Gitlab::Ci::YamlProcessor.new(config).execute + builds = config_processor.builds.select { |b| b[:stage] == "test" } - expect(config_processor.stage_builds_attributes("test").size).to eq(1) - expect(config_processor.stage_builds_attributes("test").first).to eq({ + expect(builds.size).to eq(1) + expect(builds.first).to eq({ stage: "test", stage_idx: 2, name: "rspec", @@ -174,7 +178,7 @@ module Gitlab end it 'includes retry count in build options attribute' do - expect(subject[:options]).to include(retry: { max: 1 }) + expect(rspec_build[:options]).to include(retry: { max: 1 }) end end @@ -184,7 +188,7 @@ module Gitlab end it 'does not persist retry count in the database' do - expect(subject[:options]).not_to have_key(:retry) + expect(rspec_build[:options]).not_to have_key(:retry) end end @@ -195,7 +199,7 @@ module Gitlab end it 'does use the default value' do - expect(subject[:options]).to include(retry: { max: 1 }) + expect(rspec_build[:options]).to include(retry: { max: 1 }) end end @@ -206,7 +210,7 @@ module Gitlab end it 'does use the job value' do - expect(subject[:options]).to include(retry: { max: 2 }) + expect(rspec_build[:options]).to include(retry: { max: 2 }) end end end @@ -221,7 +225,7 @@ module Gitlab end it 'is not allowed to fail' do - expect(subject[:allow_failure]).to be false + expect(rspec_build[:allow_failure]).to be false end end @@ -232,7 +236,7 @@ module Gitlab end it 'is allowed to fail' do - expect(subject[:allow_failure]).to be true + expect(rspec_build[:allow_failure]).to be true end end @@ -244,11 +248,11 @@ module Gitlab end it 'is not allowed to fail' do - expect(subject[:allow_failure]).to be false + expect(rspec_build[:allow_failure]).to be false end it 'saves allow_failure_criteria into options' do - expect(subject[:options]).to match( + expect(rspec_build[:options]).to match( a_hash_including(allow_failure_criteria: { exit_codes: [1] })) end end @@ -262,7 +266,7 @@ module Gitlab end it 'is not allowed to fail' do - expect(subject[:allow_failure]).to be false + expect(rspec_build[:allow_failure]).to be false end end @@ -272,7 +276,7 @@ module Gitlab end it 'is not allowed to fail' do - expect(subject[:allow_failure]).to be false + expect(rspec_build[:allow_failure]).to be false end end @@ -283,11 +287,11 @@ module Gitlab end it 'is not allowed to fail' do - expect(subject[:allow_failure]).to be false + expect(rspec_build[:allow_failure]).to be false end it 'saves allow_failure_criteria into options' do - expect(subject[:options]).to match( + expect(rspec_build[:options]).to match( a_hash_including(allow_failure_criteria: { exit_codes: [1] })) end end @@ -305,8 +309,8 @@ module Gitlab end it 'has the attributes' do - expect(subject[:when]).to eq 'delayed' - expect(subject[:options][:start_in]).to eq '1 day' + expect(rspec_build[:when]).to eq 'delayed' + expect(rspec_build[:options][:start_in]).to eq '1 day' end end end @@ -321,7 +325,7 @@ module Gitlab end it 'has the attributes' do - expect(subject[:resource_group_key]).to eq 'iOS' + expect(rspec_build[:resource_group_key]).to eq 'iOS' end end end @@ -337,7 +341,7 @@ module Gitlab end it 'has the attributes' do - expect(subject[:options]).to eq( + expect(rspec_build[:options]).to eq( trigger: { project: 'namespace/project', branch: 'main' } ) end @@ -353,7 +357,7 @@ module Gitlab end it 'has the attributes' do - expect(subject[:options]).to eq( + expect(rspec_build[:options]).to eq( trigger: { project: 'namespace/project', forward: { pipeline_variables: true } } ) end @@ -510,6 +514,35 @@ module Gitlab expect(subject.root_variables).to eq([]) end end + + context 'with name' do + let(:config) do + <<-EOYML + workflow: + name: 'Pipeline name' + + hello: + script: echo world + EOYML + end + + it 'parses the workflow:name as workflow_name' do + expect(subject.workflow_name).to eq('Pipeline name') + end + end + + context 'with no name' do + let(:config) do + <<-EOYML + hello: + script: echo world + EOYML + end + + it 'parses the workflow:name' do + expect(subject.workflow_name).to be_nil + end + end end describe '#warnings' do @@ -682,7 +715,7 @@ module Gitlab let(:config_data) { YAML.dump(config) } let(:config_processor) { Gitlab::Ci::YamlProcessor.new(config_data).execute } - subject { config_processor.stage_builds_attributes('test').first } + subject(:test_build) { config_processor.builds.find { |build| build[:name] == 'test' } } describe "before_script" do context "in global context" do @@ -850,9 +883,9 @@ module Gitlab rspec: { script: "rspec" } }) config_processor = Gitlab::Ci::YamlProcessor.new(config).execute + rspec_build = config_processor.builds.find { |build| build[:name] == 'rspec' } - expect(config_processor.stage_builds_attributes("test").size).to eq(1) - expect(config_processor.stage_builds_attributes("test").first).to eq({ + expect(rspec_build).to eq({ stage: "test", stage_idx: 2, name: "rspec", @@ -884,9 +917,9 @@ module Gitlab script: "rspec" } }) config_processor = Gitlab::Ci::YamlProcessor.new(config).execute + rspec_build = config_processor.builds.find { |build| build[:name] == 'rspec' } - expect(config_processor.stage_builds_attributes("test").size).to eq(1) - expect(config_processor.stage_builds_attributes("test").first).to eq({ + expect(rspec_build).to eq({ stage: "test", stage_idx: 2, name: "rspec", @@ -916,9 +949,9 @@ module Gitlab rspec: { script: "rspec" } }) config_processor = Gitlab::Ci::YamlProcessor.new(config).execute + rspec_build = config_processor.builds.find { |build| build[:name] == 'rspec' } - expect(config_processor.stage_builds_attributes("test").size).to eq(1) - expect(config_processor.stage_builds_attributes("test").first).to eq({ + expect(rspec_build).to eq({ stage: "test", stage_idx: 2, name: "rspec", @@ -944,9 +977,9 @@ module Gitlab rspec: { image: "image:1.0", services: ["postgresql", "docker:dind"], script: "rspec" } }) config_processor = Gitlab::Ci::YamlProcessor.new(config).execute + rspec_build = config_processor.builds.find { |build| build[:name] == 'rspec' } - expect(config_processor.stage_builds_attributes("test").size).to eq(1) - expect(config_processor.stage_builds_attributes("test").first).to eq({ + expect(rspec_build).to eq({ stage: "test", stage_idx: 2, name: "rspec", @@ -981,7 +1014,7 @@ module Gitlab it { is_expected.to be_valid } it "returns with image" do - expect(processor.stage_builds_attributes("test")).to contain_exactly({ + expect(processor.builds).to contain_exactly({ stage: "test", stage_idx: 2, name: "test", @@ -1014,7 +1047,7 @@ module Gitlab it { is_expected.to be_valid } it "returns with service" do - expect(processor.stage_builds_attributes("test")).to contain_exactly({ + expect(processor.builds).to contain_exactly({ stage: "test", stage_idx: 2, name: "test", @@ -1033,8 +1066,7 @@ module Gitlab end end - # Change this to a `describe` block when removing the FF ci_variables_refactoring_to_variable - shared_examples 'Variables' do + describe 'Variables' do subject(:execute) { described_class.new(config).execute } let(:build) { execute.builds.first } @@ -1163,18 +1195,6 @@ module Gitlab end end - context 'when ci_variables_refactoring_to_variable is enabled' do - it_behaves_like 'Variables' - end - - context 'when ci_variables_refactoring_to_variable is disabled' do - before do - stub_feature_flags(ci_variables_refactoring_to_variable: false) - end - - it_behaves_like 'Variables' - end - context 'when using `extends`' do let(:config_processor) { Gitlab::Ci::YamlProcessor.new(config).execute } @@ -1375,7 +1395,7 @@ module Gitlab }) config_processor = Gitlab::Ci::YamlProcessor.new(config).execute - builds = config_processor.stage_builds_attributes("test") + builds = config_processor.builds expect(builds.size).to eq(1) expect(builds.first[:when]).to eq(when_state) @@ -1391,7 +1411,7 @@ module Gitlab end it 'creates one build and sets when:' do - builds = subject.stage_builds_attributes("test") + builds = processor.builds expect(builds.size).to eq(1) expect(builds.first[:when]).to eq('delayed') @@ -1419,7 +1439,7 @@ module Gitlab end let(:config_processor) { Gitlab::Ci::YamlProcessor.new(config).execute } - let(:builds) { config_processor.stage_builds_attributes('test') } + let(:builds) { config_processor.builds } context 'when job is parallelized' do let(:parallel) { 5 } @@ -1535,15 +1555,16 @@ module Gitlab }) config_processor = Gitlab::Ci::YamlProcessor.new(config).execute + rspec_build = config_processor.builds.find { |build| build[:name] == 'rspec' } - expect(config_processor.stage_builds_attributes("test").size).to eq(1) - expect(config_processor.stage_builds_attributes("test").first[:cache]).to eq([ - paths: ["logs/", "binaries/"], - untracked: true, - key: 'key', - policy: 'pull-push', - when: 'on_success' - ]) + expect(rspec_build[:cache]).to eq( + [ + paths: ["logs/", "binaries/"], + untracked: true, + key: 'key', + policy: 'pull-push', + when: 'on_success' + ]) end it "returns cache when defined in default context" do @@ -1558,32 +1579,34 @@ module Gitlab }) config_processor = Gitlab::Ci::YamlProcessor.new(config).execute + rspec_build = config_processor.builds.find { |build| build[:name] == 'rspec' } - expect(config_processor.stage_builds_attributes("test").size).to eq(1) - expect(config_processor.stage_builds_attributes("test").first[:cache]).to eq([ - paths: ["logs/", "binaries/"], - untracked: true, - key: { files: ['file'] }, - policy: 'pull-push', - when: 'on_success' - ]) + expect(rspec_build[:cache]).to eq( + [ + paths: ["logs/", "binaries/"], + untracked: true, + key: { files: ['file'] }, + policy: 'pull-push', + when: 'on_success' + ]) end it 'returns cache key/s when defined in a job' do - config = YAML.dump({ - rspec: { - cache: [ - { paths: ['binaries/'], untracked: true, key: 'keya' }, - { paths: ['logs/', 'binaries/'], untracked: true, key: 'key' } - ], - script: 'rspec' - } - }) + config = YAML.dump( + { + rspec: { + cache: [ + { paths: ['binaries/'], untracked: true, key: 'keya' }, + { paths: ['logs/', 'binaries/'], untracked: true, key: 'key' } + ], + script: 'rspec' + } + }) config_processor = Gitlab::Ci::YamlProcessor.new(config).execute + rspec_build = config_processor.builds.find { |build| build[:name] == 'rspec' } - expect(config_processor.stage_builds_attributes('test').size).to eq(1) - expect(config_processor.stage_builds_attributes('test').first[:cache]).to eq( + expect(rspec_build[:cache]).to eq( [ { paths: ['binaries/'], @@ -1616,15 +1639,16 @@ module Gitlab ) config_processor = Gitlab::Ci::YamlProcessor.new(config).execute + rspec_build = config_processor.builds.find { |build| build[:name] == 'rspec' } - expect(config_processor.stage_builds_attributes('test').size).to eq(1) - expect(config_processor.stage_builds_attributes('test').first[:cache]).to eq([ - paths: ['binaries/'], - untracked: true, - key: { files: ['file'] }, - policy: 'pull-push', - when: 'on_success' - ]) + expect(rspec_build[:cache]).to eq( + [ + paths: ['binaries/'], + untracked: true, + key: { files: ['file'] }, + policy: 'pull-push', + when: 'on_success' + ]) end it 'returns cache files with prefix' do @@ -1640,61 +1664,65 @@ module Gitlab ) config_processor = Gitlab::Ci::YamlProcessor.new(config).execute + rspec_build = config_processor.builds.find { |build| build[:name] == 'rspec' } - expect(config_processor.stage_builds_attributes('test').size).to eq(1) - expect(config_processor.stage_builds_attributes('test').first[:cache]).to eq([ - paths: ['logs/', 'binaries/'], - untracked: true, - key: { files: ['file'], prefix: 'prefix' }, - policy: 'pull-push', - when: 'on_success' - ]) + expect(rspec_build[:cache]).to eq( + [ + paths: ['logs/', 'binaries/'], + untracked: true, + key: { files: ['file'], prefix: 'prefix' }, + policy: 'pull-push', + when: 'on_success' + ]) end it "overwrite cache when defined for a job and globally" do - config = YAML.dump({ - cache: { paths: ["logs/", "binaries/"], untracked: true, key: 'global' }, - rspec: { - script: "rspec", - cache: { paths: ["test/"], untracked: false, key: 'local' } - } - }) + config = YAML.dump( + { + cache: { paths: ["logs/", "binaries/"], untracked: true, key: 'global' }, + rspec: { + script: "rspec", + cache: { paths: ["test/"], untracked: false, key: 'local' } + } + }) config_processor = Gitlab::Ci::YamlProcessor.new(config).execute + rspec_build = config_processor.builds.find { |build| build[:name] == 'rspec' } - expect(config_processor.stage_builds_attributes("test").size).to eq(1) - expect(config_processor.stage_builds_attributes("test").first[:cache]).to eq([ - paths: ["test/"], - untracked: false, - key: 'local', - policy: 'pull-push', - when: 'on_success' - ]) + expect(rspec_build[:cache]).to eq( + [ + paths: ["test/"], + untracked: false, + key: 'local', + policy: 'pull-push', + when: 'on_success' + ]) end end describe "Artifacts" do it "returns artifacts when defined" do - config = YAML.dump({ - image: "image:1.0", - services: ["mysql"], - before_script: ["pwd"], - rspec: { - artifacts: { - paths: ["logs/", "binaries/"], - expose_as: "Exposed artifacts", - untracked: true, - name: "custom_name", - expire_in: "7d" - }, - script: "rspec" - } - }) + config = YAML.dump( + { + image: "image:1.0", + services: ["mysql"], + before_script: ["pwd"], + rspec: { + artifacts: { + paths: ["logs/", "binaries/"], + expose_as: "Exposed artifacts", + untracked: true, + name: "custom_name", + expire_in: "7d" + }, + script: "rspec" + } + }) config_processor = Gitlab::Ci::YamlProcessor.new(config).execute + rspec_build = config_processor.builds.find { |build| build[:name] == 'rspec' } - expect(config_processor.stage_builds_attributes("test").size).to eq(1) - expect(config_processor.stage_builds_attributes("test").first).to eq({ + expect(rspec_build).to eq({ stage: "test", stage_idx: 2, name: "rspec", @@ -1729,7 +1757,7 @@ module Gitlab }) config_processor = Gitlab::Ci::YamlProcessor.new(config).execute - builds = config_processor.stage_builds_attributes("test") + builds = config_processor.builds expect(builds.size).to eq(1) expect(builds.first[:options][:artifacts][:expire_in]).to eq('never') @@ -1745,7 +1773,7 @@ module Gitlab }) config_processor = Gitlab::Ci::YamlProcessor.new(config).execute - builds = config_processor.stage_builds_attributes("test") + builds = config_processor.builds expect(builds.size).to eq(1) expect(builds.first[:options][:artifacts][:when]).to eq(when_state) @@ -1778,7 +1806,7 @@ module Gitlab - my/test/something YAML - attributes = Gitlab::Ci::YamlProcessor.new(config).execute.build_attributes('test') + attributes = Gitlab::Ci::YamlProcessor.new(config).execute.builds.find { |build| build[:name] == 'test' } expect(attributes.dig(*%i[options artifacts exclude])).to eq(%w[my/test/something]) end @@ -1819,7 +1847,7 @@ module Gitlab end it "returns release info" do - expect(processor.stage_builds_attributes('release').first[:options]) + expect(processor.builds.first[:options]) .to eq(config[:release].except(:stage, :only)) end end @@ -1833,7 +1861,7 @@ module Gitlab subject { Gitlab::Ci::YamlProcessor.new(YAML.dump(config)).execute } - let(:builds) { subject.stage_builds_attributes('deploy') } + let(:builds) { subject.builds } context 'when a production environment is specified' do let(:environment) { 'production' } @@ -1943,7 +1971,7 @@ module Gitlab subject { Gitlab::Ci::YamlProcessor.new(YAML.dump(config)).execute } - let(:builds) { subject.stage_builds_attributes('deploy') } + let(:builds) { subject.builds } context 'when no timeout was provided' do it 'does not include job_timeout' do @@ -2370,8 +2398,8 @@ module Gitlab it 'returns a valid configuration and sets artifacts: true by default' do expect(subject).to be_valid - rspec = subject.build_attributes(:rspec) - expect(rspec.dig(:options, :cross_dependencies)).to eq( + rspec_build = subject.builds.find { |build| build[:name] == 'rspec' } + expect(rspec_build.dig(:options, :cross_dependencies)).to eq( [{ pipeline: '$THE_PIPELINE_ID', job: 'dependency-job', artifacts: true }] ) end @@ -2391,8 +2419,8 @@ module Gitlab it 'returns a valid configuration and sets artifacts: true by default' do expect(subject).to be_valid - rspec = subject.build_attributes(:rspec) - expect(rspec.dig(:options, :cross_dependencies)).to eq( + rspec_build = subject.builds.find { |build| build[:name] == 'rspec' } + expect(rspec_build.dig(:options, :cross_dependencies)).to eq( [{ pipeline: '123', job: 'dependency-job', artifacts: true }] ) end @@ -2422,7 +2450,7 @@ module Gitlab describe "Hidden jobs" do let(:config_processor) { Gitlab::Ci::YamlProcessor.new(config).execute } - subject { config_processor.stage_builds_attributes("test") } + subject { config_processor.builds } shared_examples 'hidden_job_handling' do it "doesn't create jobs that start with dot" do @@ -2470,7 +2498,7 @@ module Gitlab describe "YAML Alias/Anchor" do let(:config_processor) { Gitlab::Ci::YamlProcessor.new(config).execute } - subject { config_processor.stage_builds_attributes("build") } + subject { config_processor.builds } shared_examples 'job_templates_handling' do it "is correctly supported for jobs" do |