diff options
Diffstat (limited to 'spec/lib/gitlab/ci')
27 files changed, 954 insertions, 410 deletions
diff --git a/spec/lib/gitlab/ci/build/artifacts/metadata_spec.rb b/spec/lib/gitlab/ci/build/artifacts/metadata_spec.rb index efe99cd276c..1f3ba0ef76e 100644 --- a/spec/lib/gitlab/ci/build/artifacts/metadata_spec.rb +++ b/spec/lib/gitlab/ci/build/artifacts/metadata_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::Build::Artifacts::Metadata do +RSpec.describe Gitlab::Ci::Build::Artifacts::Metadata, feature_category: :build_artifacts do def metadata(path = '', **opts) described_class.new(metadata_file_stream, path, **opts) end @@ -19,132 +19,158 @@ RSpec.describe Gitlab::Ci::Build::Artifacts::Metadata do metadata_file_stream&.close end - context 'metadata file exists' do - describe '#find_entries! empty string' do - subject { metadata('').find_entries! } + describe '#to_entry' do + subject(:entry) { metadata.to_entry } - it 'matches correct paths' do - expect(subject.keys).to contain_exactly 'ci_artifacts.txt', - 'other_artifacts_0.1.2/', - 'rails_sample.jpg', - 'tests_encoding/' - end - - it 'matches metadata for every path' do - expect(subject.keys.count).to eq 4 - end + it { is_expected.to be_an_instance_of(Gitlab::Ci::Build::Artifacts::Metadata::Entry) } - it 'return Hashes for each metadata' do - expect(subject.values).to all(be_kind_of(Hash)) + context 'when given path starts with a ./ prefix' do + it 'instantiates the entry without the ./ prefix from the path' do + meta = metadata("./some/path") + expect(Gitlab::Ci::Build::Artifacts::Metadata::Entry).to receive(:new).with("some/path", {}) + meta.to_entry end end + end - describe '#find_entries! other_artifacts_0.1.2/' do - subject { metadata('other_artifacts_0.1.2/').find_entries! } + describe '#full_version' do + subject { metadata.full_version } - it 'matches correct paths' do - expect(subject.keys) - .to contain_exactly 'other_artifacts_0.1.2/', - 'other_artifacts_0.1.2/doc_sample.txt', - 'other_artifacts_0.1.2/another-subdirectory/' - end - end + it { is_expected.to eq 'GitLab Build Artifacts Metadata 0.0.1' } + end - describe '#find_entries! other_artifacts_0.1.2/another-subdirectory/' do - subject { metadata('other_artifacts_0.1.2/another-subdirectory/').find_entries! } + describe '#version' do + subject { metadata.version } - it 'matches correct paths' do - expect(subject.keys) - .to contain_exactly 'other_artifacts_0.1.2/another-subdirectory/', - 'other_artifacts_0.1.2/another-subdirectory/empty_directory/', - 'other_artifacts_0.1.2/another-subdirectory/banana_sample.gif' - end - end + it { is_expected.to eq '0.0.1' } + end - describe '#find_entries! recursively for other_artifacts_0.1.2/' do - subject { metadata('other_artifacts_0.1.2/', recursive: true).find_entries! } + describe '#errors' do + subject { metadata.errors } - it 'matches correct paths' do - expect(subject.keys) - .to contain_exactly 'other_artifacts_0.1.2/', - 'other_artifacts_0.1.2/doc_sample.txt', - 'other_artifacts_0.1.2/another-subdirectory/', - 'other_artifacts_0.1.2/another-subdirectory/empty_directory/', - 'other_artifacts_0.1.2/another-subdirectory/banana_sample.gif' - end - end + it { is_expected.to eq({}) } + end - describe '#to_entry' do - subject { metadata('').to_entry } + describe '#find_entries!' do + let(:recursive) { false } - it { is_expected.to be_an_instance_of(Gitlab::Ci::Build::Artifacts::Metadata::Entry) } - end + subject(:find_entries) { metadata(path, recursive: recursive).find_entries! } - describe '#full_version' do - subject { metadata('').full_version } + context 'when metadata file exists' do + context 'and given path is an empty string' do + let(:path) { '' } - it { is_expected.to eq 'GitLab Build Artifacts Metadata 0.0.1' } - end + it 'returns paths to all files and directories at the root level' do + expect(find_entries.keys).to contain_exactly( + 'ci_artifacts.txt', + 'other_artifacts_0.1.2/', + 'rails_sample.jpg', + 'tests_encoding/' + ) + end - describe '#version' do - subject { metadata('').version } + it 'return Hashes for each metadata' do + expect(find_entries.values).to all(be_kind_of(Hash)) + end + end - it { is_expected.to eq '0.0.1' } - end + shared_examples 'finding entries for a given path' do |options| + let(:path) { "#{options[:path_prefix]}#{target_path}" } + + context 'when given path targets a directory at the root level' do + let(:target_path) { 'other_artifacts_0.1.2/' } + + it 'returns paths to all files and directories at the first level of the directory' do + expect(find_entries.keys).to contain_exactly( + 'other_artifacts_0.1.2/', + 'other_artifacts_0.1.2/doc_sample.txt', + 'other_artifacts_0.1.2/another-subdirectory/' + ) + end + end + + context 'when given path targets a sub-directory' do + let(:target_path) { 'other_artifacts_0.1.2/another-subdirectory/' } + + it 'returns paths to all files and directories at the first level of the sub-directory' do + expect(find_entries.keys).to contain_exactly( + 'other_artifacts_0.1.2/another-subdirectory/', + 'other_artifacts_0.1.2/another-subdirectory/empty_directory/', + 'other_artifacts_0.1.2/another-subdirectory/banana_sample.gif' + ) + end + end + + context 'when given path targets a directory recursively' do + let(:target_path) { 'other_artifacts_0.1.2/' } + let(:recursive) { true } + + it 'returns all paths recursively within the target directory' do + expect(subject.keys).to contain_exactly( + 'other_artifacts_0.1.2/', + 'other_artifacts_0.1.2/doc_sample.txt', + 'other_artifacts_0.1.2/another-subdirectory/', + 'other_artifacts_0.1.2/another-subdirectory/empty_directory/', + 'other_artifacts_0.1.2/another-subdirectory/banana_sample.gif' + ) + end + end + end - describe '#errors' do - subject { metadata('').errors } + context 'and given path does not start with a ./ prefix' do + it_behaves_like 'finding entries for a given path', path_prefix: '' + end - it { is_expected.to eq({}) } + context 'and given path starts with a ./ prefix' do + it_behaves_like 'finding entries for a given path', path_prefix: './' + end end - end - context 'metadata file does not exist' do - let(:metadata_file_path) { nil } + context 'when metadata file stream is nil' do + let(:path) { '' } + let(:metadata_file_stream) { nil } - describe '#find_entries!' do it 'raises error' do - expect { metadata.find_entries! }.to raise_error(described_class::InvalidStreamError, /Invalid stream/) + expect { find_entries }.to raise_error(described_class::InvalidStreamError, /Invalid stream/) end end - end - context 'metadata file is invalid' do - let(:metadata_file_path) { Rails.root + 'spec/fixtures/ci_build_artifacts.zip' } + context 'when metadata file is invalid' do + let(:path) { '' } + let(:metadata_file_path) { Rails.root + 'spec/fixtures/ci_build_artifacts.zip' } - describe '#find_entries!' do it 'raises error' do - expect { metadata.find_entries! }.to raise_error(described_class::InvalidStreamError, /not in gzip format/) + expect { find_entries }.to raise_error(described_class::InvalidStreamError, /not in gzip format/) end end - end - context 'generated metadata' do - let(:tmpfile) { Tempfile.new('test-metadata') } - let(:generator) { CiArtifactMetadataGenerator.new(tmpfile) } - let(:entry_count) { 5 } + context 'with generated metadata' do + let(:tmpfile) { Tempfile.new('test-metadata') } + let(:generator) { CiArtifactMetadataGenerator.new(tmpfile) } + let(:entry_count) { 5 } - before do - tmpfile.binmode + before do + tmpfile.binmode - (1..entry_count).each do |index| - generator.add_entry("public/test-#{index}.txt") - end + (1..entry_count).each do |index| + generator.add_entry("public/test-#{index}.txt") + end - generator.write - end + generator.write + end - after do - File.unlink(tmpfile.path) - end + after do + File.unlink(tmpfile.path) + end - describe '#find_entries!' do - it 'reads expected number of entries' do - stream = File.open(tmpfile.path) + describe '#find_entries!' do + it 'reads expected number of entries' do + stream = File.open(tmpfile.path) - metadata = described_class.new(stream, 'public', recursive: true) + metadata = described_class.new(stream, 'public', recursive: true) - expect(metadata.find_entries!.count).to eq entry_count + expect(metadata.find_entries!.count).to eq entry_count + end end end end diff --git a/spec/lib/gitlab/ci/build/duration_parser_spec.rb b/spec/lib/gitlab/ci/build/duration_parser_spec.rb index 7f5ff1eb0ee..bc905aa0a35 100644 --- a/spec/lib/gitlab/ci/build/duration_parser_spec.rb +++ b/spec/lib/gitlab/ci/build/duration_parser_spec.rb @@ -25,8 +25,8 @@ RSpec.describe Gitlab::Ci::Build::DurationParser do it { is_expected.to be_truthy } it 'caches data' do - expect(ChronicDuration).to receive(:parse).with(value).once.and_call_original - expect(ChronicDuration).to receive(:parse).with(other_value).once.and_call_original + expect(ChronicDuration).to receive(:parse).with(value, use_complete_matcher: true).once.and_call_original + expect(ChronicDuration).to receive(:parse).with(other_value, use_complete_matcher: true).once.and_call_original 2.times do expect(described_class.validate_duration(value)).to eq(86400) @@ -41,7 +41,7 @@ RSpec.describe Gitlab::Ci::Build::DurationParser do it { is_expected.to be_falsy } it 'caches data' do - expect(ChronicDuration).to receive(:parse).with(value).once.and_call_original + expect(ChronicDuration).to receive(:parse).with(value, use_complete_matcher: true).once.and_call_original 2.times do expect(described_class.validate_duration(value)).to be_falsey diff --git a/spec/lib/gitlab/ci/components/instance_path_spec.rb b/spec/lib/gitlab/ci/components/instance_path_spec.rb index f4bc706f9b4..97843781891 100644 --- a/spec/lib/gitlab/ci/components/instance_path_spec.rb +++ b/spec/lib/gitlab/ci/components/instance_path_spec.rb @@ -14,125 +14,214 @@ RSpec.describe Gitlab::Ci::Components::InstancePath, feature_category: :pipeline end describe 'FQDN path' do - let_it_be(:existing_project) { create(:project, :repository) } - - let(:project_path) { existing_project.full_path } - let(:address) { "acme.com/#{project_path}/component@#{version}" } let(:version) { 'master' } + let(:project_path) { project.full_path } + let(:address) { "acme.com/#{project_path}/secret-detection@#{version}" } + + context 'when the project repository contains a templates directory' do + let_it_be(:project) do + create( + :project, :custom_repo, + files: { + 'templates/secret-detection.yml' => 'image: alpine_1', + 'templates/dast/template.yml' => 'image: alpine_2', + 'templates/dast/another-template.yml' => 'image: alpine_3', + 'templates/dast/another-folder/template.yml' => 'image: alpine_4' + } + ) + end - context 'when project exists' do - it 'provides the expected attributes', :aggregate_failures do - expect(path.project).to eq(existing_project) - expect(path.host).to eq(current_host) - expect(path.sha).to eq(existing_project.commit('master').id) - expect(path.project_file_path).to eq('component/template.yml') + before do + project.add_developer(user) end - context 'when content exists' do - let(:content) { 'image: alpine' } + context 'when user does not have permissions' do + it 'raises an error when fetching the content' do + expect { path.fetch_content!(current_user: build(:user)) } + .to raise_error(Gitlab::Access::AccessDeniedError) + end + end - before do - allow_next_instance_of(Repository) do |instance| - allow(instance) - .to receive(:blob_data_at) - .with(existing_project.commit('master').id, 'component/template.yml') - .and_return(content) - end + context 'when the component is simple (single file template)' do + it 'fetches the component content', :aggregate_failures do + expect(path.fetch_content!(current_user: user)).to eq('image: alpine_1') + expect(path.host).to eq(current_host) + expect(path.project_file_path).to eq('templates/secret-detection.yml') + expect(path.project).to eq(project) + expect(path.sha).to eq(project.commit('master').id) end + end - context 'when user has permissions to read code' do - before do - existing_project.add_developer(user) - end + context 'when the component is complex (directory-based template)' do + let(:address) { "acme.com/#{project_path}/dast@#{version}" } + + it 'fetches the component content', :aggregate_failures do + expect(path.fetch_content!(current_user: user)).to eq('image: alpine_2') + expect(path.host).to eq(current_host) + expect(path.project_file_path).to eq('templates/dast/template.yml') + expect(path.project).to eq(project) + expect(path.sha).to eq(project.commit('master').id) + end - it 'fetches the content' do - expect(path.fetch_content!(current_user: user)).to eq(content) + context 'when there is an invalid nested component folder' do + let(:address) { "acme.com/#{project_path}/dast/another-folder@#{version}" } + + it 'returns nil' do + expect(path.fetch_content!(current_user: user)).to be_nil end end - context 'when user does not have permissions to download code' do - it 'raises an error when fetching the content' do - expect { path.fetch_content!(current_user: user) } - .to raise_error(Gitlab::Access::AccessDeniedError) + context 'when there is an invalid nested component path' do + let(:address) { "acme.com/#{project_path}/dast/another-template@#{version}" } + + it 'returns nil' do + expect(path.fetch_content!(current_user: user)).to be_nil end end end - end - context 'when project path is nested under a subgroup' do - let(:existing_group) { create(:group, :nested) } - let(:existing_project) { create(:project, :repository, group: existing_group) } + context 'when fetching the latest version of a component' do + let_it_be(:project) do + create( + :project, :custom_repo, + files: { + 'templates/secret-detection.yml' => 'image: alpine_1' + } + ) + end - it 'provides the expected attributes', :aggregate_failures do - expect(path.project).to eq(existing_project) - expect(path.host).to eq(current_host) - expect(path.sha).to eq(existing_project.commit('master').id) - expect(path.project_file_path).to eq('component/template.yml') - end - end + let(:version) { '~latest' } - context 'when current GitLab instance is installed on a relative URL' do - let(:address) { "acme.com/gitlab/#{project_path}/component@#{version}" } - let(:current_host) { 'acme.com/gitlab/' } + let(:latest_sha) do + project.repository.commit('master').id + end - it 'provides the expected attributes', :aggregate_failures do - expect(path.project).to eq(existing_project) - expect(path.host).to eq(current_host) - expect(path.sha).to eq(existing_project.commit('master').id) - expect(path.project_file_path).to eq('component/template.yml') + before do + create(:release, project: project, sha: project.repository.root_ref_sha, + released_at: Time.zone.now - 1.day) + + project.repository.update_file( + user, 'templates/secret-detection.yml', 'image: alpine_2', + message: 'Updates image', branch_name: project.default_branch + ) + + create(:release, project: project, sha: latest_sha, + released_at: Time.zone.now) + end + + it 'fetches the component content', :aggregate_failures do + expect(path.fetch_content!(current_user: user)).to eq('image: alpine_2') + expect(path.host).to eq(current_host) + expect(path.project_file_path).to eq('templates/secret-detection.yml') + expect(path.project).to eq(project) + expect(path.sha).to eq(latest_sha) + end end - end - context 'when version does not exist' do - let(:version) { 'non-existent' } + context 'when version does not exist' do + let(:version) { 'non-existent' } - it 'provides the expected attributes', :aggregate_failures do - expect(path.project).to eq(existing_project) - expect(path.host).to eq(current_host) - expect(path.sha).to be_nil - expect(path.project_file_path).to eq('component/template.yml') + it 'returns nil', :aggregate_failures do + expect(path.fetch_content!(current_user: user)).to be_nil + expect(path.host).to eq(current_host) + expect(path.project_file_path).to be_nil + expect(path.project).to eq(project) + expect(path.sha).to be_nil + end end - it 'returns nil when fetching the content' do - expect(path.fetch_content!(current_user: user)).to be_nil + context 'when current GitLab instance is installed on a relative URL' do + let(:address) { "acme.com/gitlab/#{project_path}/secret-detection@#{version}" } + let(:current_host) { 'acme.com/gitlab/' } + + it 'fetches the component content', :aggregate_failures do + expect(path.fetch_content!(current_user: user)).to eq('image: alpine_1') + expect(path.host).to eq(current_host) + expect(path.project_file_path).to eq('templates/secret-detection.yml') + expect(path.project).to eq(project) + expect(path.sha).to eq(project.commit('master').id) + end end end - context 'when version is `~latest`' do - let(:version) { '~latest' } + # All the following tests are for deprecated code and will be removed + # in https://gitlab.com/gitlab-org/gitlab/-/issues/415855 + context 'when the project does not contain a templates directory' do + let(:project_path) { project.full_path } + let(:address) { "acme.com/#{project_path}/component@#{version}" } + + let_it_be(:project) do + create( + :project, :custom_repo, + files: { + 'component/template.yml' => 'image: alpine' + } + ) + end + + before do + project.add_developer(user) + end - context 'when project has releases' do - let_it_be(:latest_release) do - create(:release, project: existing_project, sha: 'sha-1', released_at: Time.zone.now) - end + it 'fetches the component content', :aggregate_failures do + expect(path.fetch_content!(current_user: user)).to eq('image: alpine') + expect(path.host).to eq(current_host) + expect(path.project_file_path).to eq('component/template.yml') + expect(path.project).to eq(project) + expect(path.sha).to eq(project.commit('master').id) + end - before_all do - # Previous release - create(:release, project: existing_project, sha: 'sha-2', released_at: Time.zone.now - 1.day) + context 'when project path is nested under a subgroup' do + let_it_be(:group) { create(:group, :nested) } + let_it_be(:project) do + create( + :project, :custom_repo, + files: { + 'component/template.yml' => 'image: alpine' + }, + group: group + ) end - it 'returns the sha of the latest release' do - expect(path.sha).to eq(latest_release.sha) + it 'fetches the component content', :aggregate_failures do + expect(path.fetch_content!(current_user: user)).to eq('image: alpine') + expect(path.host).to eq(current_host) + expect(path.project_file_path).to eq('component/template.yml') + expect(path.project).to eq(project) + expect(path.sha).to eq(project.commit('master').id) end end - context 'when project does not have releases' do - it { expect(path.sha).to be_nil } + context 'when current GitLab instance is installed on a relative URL' do + let(:address) { "acme.com/gitlab/#{project_path}/component@#{version}" } + let(:current_host) { 'acme.com/gitlab/' } + + it 'fetches the component content', :aggregate_failures do + expect(path.fetch_content!(current_user: user)).to eq('image: alpine') + expect(path.host).to eq(current_host) + expect(path.project_file_path).to eq('component/template.yml') + expect(path.project).to eq(project) + expect(path.sha).to eq(project.commit('master').id) + end end - end - context 'when project does not exist' do - let(:project_path) { 'non-existent/project' } + context 'when version does not exist' do + let(:version) { 'non-existent' } - it 'provides the expected attributes', :aggregate_failures do - expect(path.project).to be_nil - expect(path.host).to eq(current_host) - expect(path.sha).to be_nil - expect(path.project_file_path).to be_nil + it 'returns nil', :aggregate_failures do + expect(path.fetch_content!(current_user: user)).to be_nil + expect(path.host).to eq(current_host) + expect(path.project_file_path).to be_nil + expect(path.project).to eq(project) + expect(path.sha).to be_nil + end end - it 'returns nil when fetching the content' do - expect(path.fetch_content!(current_user: user)).to be_nil + context 'when user does not have permissions' do + it 'raises an error when fetching the content' do + expect { path.fetch_content!(current_user: build(:user)) } + .to raise_error(Gitlab::Access::AccessDeniedError) + end end end end diff --git a/spec/lib/gitlab/ci/config/entry/bridge_spec.rb b/spec/lib/gitlab/ci/config/entry/bridge_spec.rb index 736c184a289..567ffa68836 100644 --- a/spec/lib/gitlab/ci/config/entry/bridge_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/bridge_spec.rb @@ -14,7 +14,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Bridge do # as they do not have sense in context of Bridge let(:ignored_inheritable_columns) do %i[before_script after_script hooks image services cache interruptible timeout - retry tags artifacts] + retry tags artifacts id_tokens] end end diff --git a/spec/lib/gitlab/ci/config/entry/default_spec.rb b/spec/lib/gitlab/ci/config/entry/default_spec.rb index 46e96843ee3..17e716629cd 100644 --- a/spec/lib/gitlab/ci/config/entry/default_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/default_spec.rb @@ -27,7 +27,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Default do it 'contains the expected node names' do expect(described_class.nodes.keys) .to match_array(%i[before_script after_script hooks cache image services - interruptible timeout retry tags artifacts]) + interruptible timeout retry tags artifacts id_tokens]) end end end diff --git a/spec/lib/gitlab/ci/config/entry/include/rules/rule_spec.rb b/spec/lib/gitlab/ci/config/entry/include/rules/rule_spec.rb index dd15b049b9b..cd8e35ede61 100644 --- a/spec/lib/gitlab/ci/config/entry/include/rules/rule_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/include/rules/rule_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' # Change this to fast spec helper when FF `ci_refactor_external_rules` is removed +require 'fast_spec_helper' require_dependency 'active_model' RSpec.describe Gitlab::Ci::Config::Entry::Include::Rules::Rule, feature_category: :pipeline_composition do @@ -14,21 +14,11 @@ RSpec.describe Gitlab::Ci::Config::Entry::Include::Rules::Rule, feature_category entry.compose! end - shared_examples 'a valid config' do + shared_examples 'a valid config' do |expected_value = nil| it { is_expected.to be_valid } it 'returns the expected value' do - expect(entry.value).to eq(config.compact) - end - - context 'when FF `ci_refactor_external_rules` is disabled' do - before do - stub_feature_flags(ci_refactor_external_rules: false) - end - - it 'returns the expected value' do - expect(entry.value).to eq(config) - end + expect(entry.value).to eq(expected_value || config.compact) end end @@ -99,19 +89,37 @@ RSpec.describe Gitlab::Ci::Config::Entry::Include::Rules::Rule, feature_category it_behaves_like 'a valid config' - context 'when array' do + context 'when exists: clause is an array' do let(:config) { { exists: ['./this.md', './that.md'] } } it_behaves_like 'a valid config' end - context 'when null' do + context 'when exists: clause is null' do let(:config) { { exists: nil } } it_behaves_like 'a valid config' end end + context 'when specifying a changes: clause' do + let(:config) { { changes: %w[Dockerfile lib/* paths/**/*.rb] } } + + it_behaves_like 'a valid config', { changes: { paths: %w[Dockerfile lib/* paths/**/*.rb] } } + + context 'with paths:' do + let(:config) { { changes: { paths: %w[Dockerfile lib/* paths/**/*.rb] } } } + + it_behaves_like 'a valid config' + end + + context 'with paths: and compare_to:' do + let(:config) { { changes: { paths: ['Dockerfile'], compare_to: 'branch1' } } } + + it_behaves_like 'a valid config' + end + end + context 'when specifying an unknown keyword' do let(:config) { { invalid: :something } } diff --git a/spec/lib/gitlab/ci/config/entry/include/rules_spec.rb b/spec/lib/gitlab/ci/config/entry/include/rules_spec.rb index 05db81abfc1..503020e2202 100644 --- a/spec/lib/gitlab/ci/config/entry/include/rules_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/include/rules_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'spec_helper' # Change this to fast spec helper when FF `ci_refactor_external_rules` is removed +require 'fast_spec_helper' require_dependency 'active_model' RSpec.describe Gitlab::Ci::Config::Entry::Include::Rules, feature_category: :pipeline_composition do @@ -50,7 +50,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Include::Rules, feature_category: :pip entry.compose! end - it_behaves_like 'an invalid config', /contains unknown keys: changes/ + it_behaves_like 'a valid config' end end @@ -80,7 +80,8 @@ RSpec.describe Gitlab::Ci::Config::Entry::Include::Rules, feature_category: :pip let(:config) do [ { if: '$THIS == "that"' }, - { if: '$SKIP', when: 'never' } + { if: '$SKIP', when: 'never' }, + { changes: ['Dockerfile'] } ] end @@ -96,7 +97,8 @@ RSpec.describe Gitlab::Ci::Config::Entry::Include::Rules, feature_category: :pip is_expected.to eq( [ { if: '$THIS == "that"' }, - { if: '$SKIP', when: 'never' } + { if: '$SKIP', when: 'never' }, + { changes: { paths: ['Dockerfile'] } } ] ) end @@ -115,30 +117,5 @@ RSpec.describe Gitlab::Ci::Config::Entry::Include::Rules, feature_category: :pip end end end - - context 'when FF `ci_refactor_external_rules` is disabled' do - before do - stub_feature_flags(ci_refactor_external_rules: false) - end - - context 'with an "if"' do - let(:config) do - [{ if: '$THIS == "that"' }] - end - - it { is_expected.to eq(config) } - end - - context 'with a list of two rules' do - let(:config) do - [ - { if: '$THIS == "that"' }, - { if: '$SKIP' } - ] - end - - it { is_expected.to eq(config) } - end - end end end diff --git a/spec/lib/gitlab/ci/config/entry/job_spec.rb b/spec/lib/gitlab/ci/config/entry/job_spec.rb index 4be7c11fab0..1a78d929871 100644 --- a/spec/lib/gitlab/ci/config/entry/job_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/job_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' RSpec.describe Gitlab::Ci::Config::Entry::Job, feature_category: :pipeline_composition do + using RSpec::Parameterized::TableSyntax + let(:entry) { described_class.new(config, name: :rspec) } it_behaves_like 'with inheritable CI config' do @@ -29,7 +31,7 @@ RSpec.describe Gitlab::Ci::Config::Entry::Job, feature_category: :pipeline_compo let(:result) do %i[before_script script after_script hooks stage cache image services only except rules needs variables artifacts - environment coverage retry interruptible timeout release tags + coverage retry interruptible timeout release tags inherit parallel] end @@ -696,8 +698,6 @@ RSpec.describe Gitlab::Ci::Config::Entry::Job, feature_category: :pipeline_compo end context 'with workflow rules' do - using RSpec::Parameterized::TableSyntax - where(:name, :has_workflow_rules?, :only, :rules, :result) do "uses default only" | false | nil | nil | { refs: %w[branches tags] } "uses user only" | false | %w[branches] | nil | { refs: %w[branches] } @@ -739,6 +739,20 @@ RSpec.describe Gitlab::Ci::Config::Entry::Job, feature_category: :pipeline_compo end end + describe '#pages_job?', :aggregate_failures, feature_category: :pages do + where(:name, :result) do + :pages | true + :'pages:staging' | false + :'something:pages:else' | false + end + + with_them do + subject { described_class.new({}, name: name).pages_job? } + + it { is_expected.to eq(result) } + end + end + context 'when composed' do before do entry.compose! diff --git a/spec/lib/gitlab/ci/config/entry/processable_spec.rb b/spec/lib/gitlab/ci/config/entry/processable_spec.rb index 4f13940d7e2..132e75a808b 100644 --- a/spec/lib/gitlab/ci/config/entry/processable_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/processable_spec.rb @@ -371,6 +371,39 @@ RSpec.describe Gitlab::Ci::Config::Entry::Processable, feature_category: :pipeli end end + context 'with environment' do + context 'when environment name is specified' do + let(:config) { { script: 'ls', environment: 'prod' }.compact } + + it 'sets environment name and action to the entry value' do + entry.compose!(deps) + + expect(entry.value[:environment]).to eq({ action: 'start', name: 'prod' }) + expect(entry.value[:environment_name]).to eq('prod') + end + end + + context 'when environment name, url and action are specified' do + let(:config) do + { + script: 'ls', + environment: { + name: 'staging', + url: 'https://gitlab.com', + action: 'prepare' + } + }.compact + end + + it 'sets environment name, action and url to the entry value' do + entry.compose!(deps) + + expect(entry.value[:environment]).to eq({ action: 'prepare', name: 'staging', url: 'https://gitlab.com' }) + expect(entry.value[:environment_name]).to eq('staging') + end + end + end + context 'with inheritance' do context 'of default:tags' do using RSpec::Parameterized::TableSyntax diff --git a/spec/lib/gitlab/ci/config/external/context_spec.rb b/spec/lib/gitlab/ci/config/external/context_spec.rb index d8bd578be94..9ac72ebbac8 100644 --- a/spec/lib/gitlab/ci/config/external/context_spec.rb +++ b/spec/lib/gitlab/ci/config/external/context_spec.rb @@ -4,6 +4,7 @@ require 'spec_helper' RSpec.describe Gitlab::Ci::Config::External::Context, feature_category: :pipeline_composition do let(:project) { build(:project) } + let(:pipeline) { double('Pipeline') } let(:user) { double('User') } let(:sha) { '12345' } let(:variables) { Gitlab::Ci::Variables::Collection.new([{ 'key' => 'a', 'value' => 'b' }]) } @@ -11,6 +12,7 @@ RSpec.describe Gitlab::Ci::Config::External::Context, feature_category: :pipelin let(:attributes) do { project: project, + pipeline: pipeline, user: user, sha: sha, variables: variables, @@ -32,7 +34,7 @@ RSpec.describe Gitlab::Ci::Config::External::Context, feature_category: :pipelin end context 'without values' do - let(:attributes) { { project: nil, user: nil, sha: nil } } + let(:attributes) { { project: nil, pipeline: nil, user: nil, sha: nil } } it { is_expected.to have_attributes(**attributes) } it { expect(subject.expandset).to eq([]) } @@ -148,6 +150,7 @@ RSpec.describe Gitlab::Ci::Config::External::Context, feature_category: :pipelin let(:attributes) do { project: project, + pipeline: pipeline, user: user, sha: sha, logger: double('logger') @@ -165,6 +168,7 @@ RSpec.describe Gitlab::Ci::Config::External::Context, feature_category: :pipelin it { expect(mutated).not_to eq(subject) } it { expect(mutated).to be_a(described_class) } it { expect(mutated).to have_attributes(new_attributes) } + it { expect(mutated.pipeline).to eq(subject.pipeline) } it { expect(mutated.expandset).to eq(subject.expandset) } it { expect(mutated.execution_deadline).to eq(mutated.execution_deadline) } it { expect(mutated.logger).to eq(mutated.logger) } diff --git a/spec/lib/gitlab/ci/config/external/file/component_spec.rb b/spec/lib/gitlab/ci/config/external/file/component_spec.rb index 487690296b5..0f7b811b5df 100644 --- a/spec/lib/gitlab/ci/config/external/file/component_spec.rb +++ b/spec/lib/gitlab/ci/config/external/file/component_spec.rb @@ -120,6 +120,41 @@ RSpec.describe Gitlab::Ci::Config::External::File::Component, feature_category: end end + describe '#content' do + context 'when component is valid' do + let(:content) do + <<~COMPONENT + job: + script: echo + COMPONENT + end + + let(:response) do + ServiceResponse.success(payload: { + content: content, + path: instance_double(::Gitlab::Ci::Components::InstancePath, project: project, sha: '12345') + }) + end + + it 'tracks the event' do + expect(::Gitlab::UsageDataCounters::HLLRedisCounter).to receive(:track_event).with('cicd_component_usage', + values: external_resource.context.user.id) + + external_resource.content + end + end + + context 'when component is invalid' do + let(:content) { 'the-content' } + + it 'does not track the event' do + expect(::Gitlab::UsageDataCounters::HLLRedisCounter).not_to receive(:track_event) + + external_resource.content + end + end + end + describe '#metadata' do subject(:metadata) { external_resource.metadata } diff --git a/spec/lib/gitlab/ci/config/external/mapper/verifier_spec.rb b/spec/lib/gitlab/ci/config/external/mapper/verifier_spec.rb index 69b0524be9e..f542c0485e0 100644 --- a/spec/lib/gitlab/ci/config/external/mapper/verifier_spec.rb +++ b/spec/lib/gitlab/ci/config/external/mapper/verifier_spec.rb @@ -409,32 +409,6 @@ RSpec.describe Gitlab::Ci::Config::External::Mapper::Verifier, feature_category: expect { process }.to raise_error(expected_error_class) end end - - context 'when introduce_ci_max_total_yaml_size_bytes is disabled' do - before do - stub_feature_flags(introduce_ci_max_total_yaml_size_bytes: false) - end - - context 'when pipeline tree size is within the limit' do - before do - stub_application_setting(ci_max_total_yaml_size_bytes: 10000) - end - - it 'passes the verification' do - expect(process.all?(&:valid?)).to be_truthy - end - end - - context 'when pipeline tree size is larger then the limit' do - before do - stub_application_setting(ci_max_total_yaml_size_bytes: 100) - end - - it 'passes the verification' do - expect(process.all?(&:valid?)).to be_truthy - end - end - end end end end diff --git a/spec/lib/gitlab/ci/config/external/processor_spec.rb b/spec/lib/gitlab/ci/config/external/processor_spec.rb index 19113ce6a4e..68cdf56f198 100644 --- a/spec/lib/gitlab/ci/config/external/processor_spec.rb +++ b/spec/lib/gitlab/ci/config/external/processor_spec.rb @@ -557,21 +557,11 @@ RSpec.describe Gitlab::Ci::Config::External::Processor, feature_category: :pipel context 'when rules defined' do context 'when a rule is invalid' do let(:values) do - { include: [{ local: 'builds.yml', rules: [{ changes: ['$MY_VAR'] }] }] } + { include: [{ local: 'builds.yml', rules: [{ allow_failure: ['$MY_VAR'] }] }] } end it 'raises IncludeError' do - expect { subject }.to raise_error(described_class::IncludeError, /contains unknown keys: changes/) - end - - context 'when FF `ci_refactor_external_rules` is disabled' do - before do - stub_feature_flags(ci_refactor_external_rules: false) - end - - it 'raises IncludeError' do - expect { subject }.to raise_error(described_class::IncludeError, /invalid include rule/) - end + expect { subject }.to raise_error(described_class::IncludeError, /contains unknown keys: allow_failure/) end end end diff --git a/spec/lib/gitlab/ci/config/external/rules_spec.rb b/spec/lib/gitlab/ci/config/external/rules_spec.rb index 8674af7ab65..15d7801ff2a 100644 --- a/spec/lib/gitlab/ci/config/external/rules_spec.rb +++ b/spec/lib/gitlab/ci/config/external/rules_spec.rb @@ -4,76 +4,45 @@ require 'spec_helper' RSpec.describe Gitlab::Ci::Config::External::Rules, feature_category: :pipeline_composition do let(:context) { double(variables_hash: {}) } - let(:rule_hashes) { [{ if: '$MY_VAR == "hello"' }] } + let(:rule_hashes) {} + let(:pipeline) { instance_double(Ci::Pipeline) } + let_it_be(:project) { create(:project, :custom_repo, files: { 'file.txt' => 'file' }) } subject(:rules) { described_class.new(rule_hashes) } + before do + allow(context).to receive(:project).and_return(project) + allow(context).to receive(:pipeline).and_return(pipeline) + end + describe '#evaluate' do subject(:result) { rules.evaluate(context).pass? } context 'when there is no rule' do - let(:rule_hashes) {} - it { is_expected.to eq(true) } end - shared_examples 'when there is a rule with if' do |rule_matched_result = true, rule_not_matched_result = false| - context 'when the rule matches' do - let(:context) { double(variables_hash: { 'MY_VAR' => 'hello' }) } - - it { is_expected.to eq(rule_matched_result) } - end - - context 'when the rule does not match' do - let(:context) { double(variables_hash: { 'MY_VAR' => 'invalid' }) } - - it { is_expected.to eq(rule_not_matched_result) } - end - end - - shared_examples 'when there is a rule with exists' do |file_exists_result = true, file_not_exists_result = false| - let(:project) { create(:project, :repository) } - - context 'when the file exists' do - let(:context) { double(project: project, sha: project.repository.tree.sha, top_level_worktree_paths: ['Dockerfile']) } - + shared_examples 'with when: specified' do + context 'with when: never' do before do - project.repository.create_file(project.first_owner, 'Dockerfile', "commit", message: 'test', branch_name: "master") + rule_hashes.first[:when] = 'never' end - it { is_expected.to eq(file_exists_result) } - end - - context 'when the file does not exist' do - let(:context) { double(project: project, sha: project.repository.tree.sha, top_level_worktree_paths: ['test.md']) } - - it { is_expected.to eq(file_not_exists_result) } - end - end - - it_behaves_like 'when there is a rule with if' - - context 'when there is a rule with exists' do - let(:rule_hashes) { [{ exists: 'Dockerfile' }] } - - it_behaves_like 'when there is a rule with exists' - end - - context 'when there is a rule with if and when' do - context 'with when: never' do - let(:rule_hashes) { [{ if: '$MY_VAR == "hello"', when: 'never' }] } - - it_behaves_like 'when there is a rule with if', false, false + it { is_expected.to eq(false) } end context 'with when: always' do - let(:rule_hashes) { [{ if: '$MY_VAR == "hello"', when: 'always' }] } + before do + rule_hashes.first[:when] = 'always' + end - it_behaves_like 'when there is a rule with if' + it { is_expected.to eq(true) } end context 'with when: <invalid string>' do - let(:rule_hashes) { [{ if: '$MY_VAR == "hello"', when: 'on_success' }] } + before do + rule_hashes.first[:when] = 'on_success' + end it 'raises an error' do expect { result }.to raise_error(described_class::InvalidIncludeRulesError, /when unknown value: on_success/) @@ -81,132 +50,125 @@ RSpec.describe Gitlab::Ci::Config::External::Rules, feature_category: :pipeline_ end context 'with when: null' do - let(:rule_hashes) { [{ if: '$MY_VAR == "hello"', when: nil }] } + before do + rule_hashes.first[:when] = nil + end - it_behaves_like 'when there is a rule with if' + it { is_expected.to eq(true) } end end - context 'when there is a rule with exists and when' do - context 'with when: never' do - let(:rule_hashes) { [{ exists: 'Dockerfile', when: 'never' }] } + context 'when there is a rule with if:' do + let(:rule_hashes) { [{ if: '$MY_VAR == "hello"' }] } - it_behaves_like 'when there is a rule with exists', false, false - end + context 'when the rule matches' do + let(:context) { double(variables_hash: { 'MY_VAR' => 'hello' }) } - context 'with when: always' do - let(:rule_hashes) { [{ exists: 'Dockerfile', when: 'always' }] } + it { is_expected.to eq(true) } - it_behaves_like 'when there is a rule with exists' + it_behaves_like 'with when: specified' end - context 'with when: <invalid string>' do - let(:rule_hashes) { [{ exists: 'Dockerfile', when: 'on_success' }] } + context 'when the rule does not match' do + let(:context) { double(variables_hash: { 'MY_VAR' => 'invalid' }) } - it 'raises an error' do - expect { result }.to raise_error(described_class::InvalidIncludeRulesError, /when unknown value: on_success/) - end + it { is_expected.to eq(false) } end + end - context 'with when: null' do - let(:rule_hashes) { [{ exists: 'Dockerfile', when: nil }] } + context 'when there is a rule with exists:' do + let(:rule_hashes) { [{ exists: 'file.txt' }] } - it_behaves_like 'when there is a rule with exists' + context 'when the file exists' do + let(:context) { double(top_level_worktree_paths: ['file.txt']) } + + it { is_expected.to eq(true) } + + it_behaves_like 'with when: specified' end - end - context 'when there is a rule with changes' do - let(:rule_hashes) { [{ changes: ['$MY_VAR'] }] } + context 'when the file does not exist' do + let(:context) { double(top_level_worktree_paths: ['README.md']) } - it 'raises an error' do - expect { result }.to raise_error(described_class::InvalidIncludeRulesError, /contains unknown keys: changes/) + it { is_expected.to eq(false) } end end - context 'when FF `ci_refactor_external_rules` is disabled' do - before do - stub_feature_flags(ci_refactor_external_rules: false) - end + context 'when there is a rule with changes:' do + let(:rule_hashes) { [{ changes: ['file.txt'] }] } - context 'when there is no rule' do - let(:rule_hashes) {} + shared_examples 'when the pipeline has modified paths' do + let(:modified_paths) { ['file.txt'] } - it { is_expected.to eq(true) } - end + before do + allow(pipeline).to receive(:modified_paths).and_return(modified_paths) + end - it_behaves_like 'when there is a rule with if' + context 'when the file has changed' do + it { is_expected.to eq(true) } - context 'when there is a rule with exists' do - let(:rule_hashes) { [{ exists: 'Dockerfile' }] } + it_behaves_like 'with when: specified' + end - it_behaves_like 'when there is a rule with exists' + context 'when the file has not changed' do + let(:modified_paths) { ['README.md'] } + + it { is_expected.to eq(false) } + end end - context 'when there is a rule with if and when' do - context 'with when: never' do - let(:rule_hashes) { [{ if: '$MY_VAR == "hello"', when: 'never' }] } + it_behaves_like 'when the pipeline has modified paths' - it_behaves_like 'when there is a rule with if', false, false - end + context 'with paths: specified' do + let(:rule_hashes) { [{ changes: { paths: ['file.txt'] } }] } - context 'with when: always' do - let(:rule_hashes) { [{ if: '$MY_VAR == "hello"', when: 'always' }] } + it_behaves_like 'when the pipeline has modified paths' + end - it_behaves_like 'when there is a rule with if' - end + context 'with paths: and compare_to: specified' do + before_all do + project.repository.add_branch(project.owner, 'branch1', 'master') - context 'with when: <invalid string>' do - let(:rule_hashes) { [{ if: '$MY_VAR == "hello"', when: 'on_success' }] } + project.repository.update_file( + project.owner, 'file.txt', 'file updated', message: 'Update file.txt', branch_name: 'branch1' + ) - it 'raises an error' do - expect { result }.to raise_error(described_class::InvalidIncludeRulesError, - 'invalid include rule: {:if=>"$MY_VAR == \"hello\"", :when=>"on_success"}') - end + project.repository.add_branch(project.owner, 'branch2', 'branch1') end - context 'with when: null' do - let(:rule_hashes) { [{ if: '$MY_VAR == "hello"', when: nil }] } - - it_behaves_like 'when there is a rule with if' + let_it_be(:pipeline) do + build(:ci_pipeline, project: project, ref: 'branch2', sha: project.commit('branch2').sha) end - end - context 'when there is a rule with exists and when' do - context 'with when: never' do - let(:rule_hashes) { [{ exists: 'Dockerfile', when: 'never' }] } + context 'when the file has changed compared to the given ref' do + let(:rule_hashes) { [{ changes: { paths: ['file.txt'], compare_to: 'master' } }] } + + it { is_expected.to eq(true) } - it_behaves_like 'when there is a rule with exists', false, false + it_behaves_like 'with when: specified' end - context 'with when: always' do - let(:rule_hashes) { [{ exists: 'Dockerfile', when: 'always' }] } + context 'when the file has not changed compared to the given ref' do + let(:rule_hashes) { [{ changes: { paths: ['file.txt'], compare_to: 'branch1' } }] } - it_behaves_like 'when there is a rule with exists' + it { is_expected.to eq(false) } end - context 'with when: <invalid string>' do - let(:rule_hashes) { [{ exists: 'Dockerfile', when: 'on_success' }] } + context 'when compare_to: is invalid' do + let(:rule_hashes) { [{ changes: { paths: ['file.txt'], compare_to: 'invalid' } }] } it 'raises an error' do - expect { result }.to raise_error(described_class::InvalidIncludeRulesError, - 'invalid include rule: {:exists=>"Dockerfile", :when=>"on_success"}') + expect { result }.to raise_error(described_class::InvalidIncludeRulesError, /compare_to is not a valid ref/) end end - - context 'with when: null' do - let(:rule_hashes) { [{ exists: 'Dockerfile', when: nil }] } - - it_behaves_like 'when there is a rule with exists' - end end + end - context 'when there is a rule with changes' do - let(:rule_hashes) { [{ changes: ['$MY_VAR'] }] } + context 'when there is a rule with an invalid key' do + let(:rule_hashes) { [{ invalid: ['$MY_VAR'] }] } - it 'raises an error' do - expect { result }.to raise_error(described_class::InvalidIncludeRulesError, - 'invalid include rule: {:changes=>["$MY_VAR"]}') - end + it 'raises an error' do + expect { result }.to raise_error(described_class::InvalidIncludeRulesError, /contains unknown keys: invalid/) end end end diff --git a/spec/lib/gitlab/ci/config/interpolation/interpolator_spec.rb b/spec/lib/gitlab/ci/config/interpolation/interpolator_spec.rb index 7bb09d35064..804164c933a 100644 --- a/spec/lib/gitlab/ci/config/interpolation/interpolator_spec.rb +++ b/spec/lib/gitlab/ci/config/interpolation/interpolator_spec.rb @@ -57,7 +57,8 @@ RSpec.describe Gitlab::Ci::Config::Interpolation::Interpolator, feature_category expect(subject).not_to be_valid expect(subject.error_message).to eq subject.errors.first - expect(subject.errors).to include('unknown input arguments') + expect(subject.errors).to include('Given inputs not defined in the `spec` section of the included ' \ + 'configuration file') end end diff --git a/spec/lib/gitlab/ci/config/yaml/tags/reference_spec.rb b/spec/lib/gitlab/ci/config/yaml/tags/reference_spec.rb index bf89942bf14..0af1b721eb6 100644 --- a/spec/lib/gitlab/ci/config/yaml/tags/reference_spec.rb +++ b/spec/lib/gitlab/ci/config/yaml/tags/reference_spec.rb @@ -2,9 +2,9 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::Config::Yaml::Tags::Reference do +RSpec.describe Gitlab::Ci::Config::Yaml::Tags::Reference, feature_category: :pipeline_composition do let(:config) do - Gitlab::Ci::Config::Yaml.load!(yaml) + Gitlab::Ci::Config::Yaml::Loader.new(yaml).load.content end describe '.tag' do diff --git a/spec/lib/gitlab/ci/config/yaml/tags/resolver_spec.rb b/spec/lib/gitlab/ci/config/yaml/tags/resolver_spec.rb index 594242c33cc..74d7513ebdf 100644 --- a/spec/lib/gitlab/ci/config/yaml/tags/resolver_spec.rb +++ b/spec/lib/gitlab/ci/config/yaml/tags/resolver_spec.rb @@ -2,9 +2,9 @@ require 'spec_helper' -RSpec.describe Gitlab::Ci::Config::Yaml::Tags::Resolver do +RSpec.describe Gitlab::Ci::Config::Yaml::Tags::Resolver, feature_category: :pipeline_composition do let(:config) do - Gitlab::Ci::Config::Yaml.load!(yaml) + Gitlab::Ci::Config::Yaml::Loader.new(yaml).load.content end describe '#to_hash' do diff --git a/spec/lib/gitlab/ci/parsers/sbom/cyclonedx_spec.rb b/spec/lib/gitlab/ci/parsers/sbom/cyclonedx_spec.rb index d06537ac330..a331af9a9ac 100644 --- a/spec/lib/gitlab/ci/parsers/sbom/cyclonedx_spec.rb +++ b/spec/lib/gitlab/ci/parsers/sbom/cyclonedx_spec.rb @@ -3,18 +3,20 @@ require 'spec_helper' RSpec.describe Gitlab::Ci::Parsers::Sbom::Cyclonedx, feature_category: :dependency_management do - let(:report) { instance_double('Gitlab::Ci::Reports::Sbom::Report') } + let(:report) { Gitlab::Ci::Reports::Sbom::Report.new } let(:report_data) { base_report_data } let(:raw_report_data) { report_data.to_json } let(:report_valid?) { true } let(:validator_errors) { [] } let(:properties_parser) { class_double('Gitlab::Ci::Parsers::Sbom::CyclonedxProperties') } + let(:uuid) { 'c9d550a3-feb8-483b-a901-5aa892d039f9' } let(:base_report_data) do { 'bomFormat' => 'CycloneDX', 'specVersion' => '1.4', - 'version' => 1 + 'version' => 1, + 'serialNumber' => "urn:uuid:#{uuid}" } end @@ -28,6 +30,7 @@ RSpec.describe Gitlab::Ci::Parsers::Sbom::Cyclonedx, feature_category: :dependen allow(properties_parser).to receive(:parse_source) stub_const('Gitlab::Ci::Parsers::Sbom::CyclonedxProperties', properties_parser) + allow(SecureRandom).to receive(:uuid).and_return(uuid) end context 'when report JSON is invalid' do @@ -149,8 +152,22 @@ RSpec.describe Gitlab::Ci::Parsers::Sbom::Cyclonedx, feature_category: :dependen end end - context 'when report has metadata properties' do - let(:report_data) { base_report_data.merge({ 'metadata' => { 'properties' => properties } }) } + context 'when report has metadata tools, author and properties' do + let(:report_data) { base_report_data.merge(metadata) } + + let(:tools) do + [ + { name: 'Gemnasium', vendor: 'vendor-1', version: '2.34.0' }, + { name: 'Gemnasium', vendor: 'vendor-2', version: '2.34.0' } + ] + end + + let(:authors) do + [ + { name: 'author-1', email: 'support@gitlab.com' }, + { name: 'author-2', email: 'support@gitlab.com' } + ] + end let(:properties) do [ @@ -163,10 +180,44 @@ RSpec.describe Gitlab::Ci::Parsers::Sbom::Cyclonedx, feature_category: :dependen ] end - it 'passes them to the properties parser' do - expect(properties_parser).to receive(:parse_source).with(properties) + context 'when metadata attributes are present' do + let(:metadata) do + { + 'metadata' => { + 'tools' => tools, + 'authors' => authors, + 'properties' => properties + } + } + end - parse! + it 'passes them to the report' do + expect(properties_parser).to receive(:parse_source).with(properties) + + parse! + + expect(report.metadata).to have_attributes( + tools: tools.map(&:with_indifferent_access), + authors: authors.map(&:with_indifferent_access), + properties: properties.map(&:with_indifferent_access) + ) + end + end + + context 'when metadata attributes are not present' do + let(:metadata) { { 'metadata' => {} } } + + it 'passes them to the report' do + expect(properties_parser).to receive(:parse_source).with(nil) + + parse! + + expect(report.metadata).to have_attributes( + tools: [], + authors: [], + properties: [] + ) + end 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 dc16ddf4e0e..9470d59f502 100644 --- a/spec/lib/gitlab/ci/parsers/security/common_spec.rb +++ b/spec/lib/gitlab/ci/parsers/security/common_spec.rb @@ -229,8 +229,9 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Common, feature_category: :vulnera describe 'parsing finding.details' do context 'when details are provided' do + let(:finding) { report.findings[4] } + it 'sets details from the report' do - finding = report.findings.find { |x| x.compare_key == 'CVE-1020' } expected_details = Gitlab::Json.parse(finding.raw_metadata)['details'] expect(finding.details).to eq(expected_details) @@ -238,8 +239,9 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Common, feature_category: :vulnera end context 'when details are not provided' do + let(:finding) { report.findings[5] } + it 'sets empty hash' do - finding = report.findings.find { |x| x.compare_key == 'CVE-1030' } expect(finding.details).to eq({}) end end diff --git a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb index 5f87e0ccc33..54e569f424b 100644 --- a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb @@ -1081,6 +1081,126 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build, feature_category: :pipeline_co end end + context 'with a rule using CI_ENVIRONMENT_ACTION variable' do + let(:rule_set) do + [{ if: '$CI_ENVIRONMENT_ACTION == "start"' }] + end + + context 'when environment:action satisfies the rule' do + let(:attributes) do + { name: 'rspec', rules: rule_set, environment: 'test', when: 'on_success', + options: { environment: { action: 'start' } } } + end + + it { is_expected.to be_included } + + it 'correctly populates when:' do + expect(seed_build.attributes).to include(when: 'on_success') + end + end + + context 'when environment:action does not satisfy rule' do + let(:attributes) do + { name: 'rspec', rules: rule_set, environment: 'test', when: 'on_success', + options: { environment: { action: 'stop' } } } + end + + it { is_expected.not_to be_included } + + it 'correctly populates when:' do + expect(seed_build.attributes).to include(when: 'never') + end + end + + context 'when environment:action is not set' do + it { is_expected.not_to be_included } + + it 'correctly populates when:' do + expect(seed_build.attributes).to include(when: 'never') + end + end + end + + context 'with a rule using CI_ENVIRONMENT_TIER variable' do + let(:rule_set) do + [{ if: '$CI_ENVIRONMENT_TIER == "production"' }] + end + + context 'when environment:deployment_tier satisfies the rule' do + let(:attributes) do + { name: 'rspec', rules: rule_set, environment: 'test', when: 'on_success', + options: { environment: { deployment_tier: 'production' } } } + end + + it { is_expected.to be_included } + + it 'correctly populates when:' do + expect(seed_build.attributes).to include(when: 'on_success') + end + end + + context 'when environment:deployment_tier does not satisfy rule' do + let(:attributes) do + { name: 'rspec', rules: rule_set, environment: 'test', when: 'on_success', + options: { environment: { deployment_tier: 'development' } } } + end + + it { is_expected.not_to be_included } + + it 'correctly populates when:' do + expect(seed_build.attributes).to include(when: 'never') + end + end + + context 'when environment:action is not set' do + it { is_expected.not_to be_included } + + it 'correctly populates when:' do + expect(seed_build.attributes).to include(when: 'never') + end + end + end + + context 'with a rule using CI_ENVIRONMENT_URL variable' do + let(:rule_set) do + [{ if: '$CI_ENVIRONMENT_URL == "http://gitlab.com"' }] + end + + context 'when environment:url satisfies the rule' do + let(:attributes) do + { name: 'rspec', rules: rule_set, environment: 'test', when: 'on_success', + options: { environment: { url: 'http://gitlab.com' } } } + end + + it { is_expected.to be_included } + + it 'correctly populates when:' do + expect(seed_build.attributes).to include(when: 'on_success') + end + end + + context 'when environment:url does not satisfy rule' do + let(:attributes) do + { name: 'rspec', rules: rule_set, environment: 'test', when: 'on_success', + options: { environment: { url: 'http://staging.gitlab.com' } } } + end + + it { is_expected.not_to be_included } + + it 'correctly populates when:' do + expect(seed_build.attributes).to include(when: 'never') + end + end + + context 'when environment:action is not set' do + it { is_expected.not_to be_included } + + it 'correctly populates when:' do + expect(seed_build.attributes).to include(when: 'never') + end + end + end + context 'with no rules' do let(:rule_set) { [] } diff --git a/spec/lib/gitlab/ci/reports/sbom/component_spec.rb b/spec/lib/gitlab/ci/reports/sbom/component_spec.rb index d62d25aeefe..4c9fd00f96a 100644 --- a/spec/lib/gitlab/ci/reports/sbom/component_spec.rb +++ b/spec/lib/gitlab/ci/reports/sbom/component_spec.rb @@ -49,6 +49,18 @@ RSpec.describe Gitlab::Ci::Reports::Sbom::Component, feature_category: :dependen end end + describe '#purl_type' do + subject { component.purl_type } + + it { is_expected.to eq(purl_type) } + end + + describe '#type' do + subject { component.type } + + it { is_expected.to eq(component_type) } + end + describe '#<=>' do where do { diff --git a/spec/lib/gitlab/ci/reports/sbom/metadata_spec.rb b/spec/lib/gitlab/ci/reports/sbom/metadata_spec.rb new file mode 100644 index 00000000000..fe0b9481039 --- /dev/null +++ b/spec/lib/gitlab/ci/reports/sbom/metadata_spec.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Reports::Sbom::Metadata, feature_category: :dependency_management do + let(:tools) do + [ + { + vendor: "vendor", + name: "Gemnasium", + version: "2.34.0" + } + ] + end + + let(:authors) do + [ + { + name: "author_name", + email: "support@gitlab.com" + } + ] + end + + let(:properties) do + [ + { + name: "property_name", + value: "package-lock.json" + } + ] + end + + let(:timestamp) { "2020-04-13T20:20:39+00:00" } + + subject(:metadata) do + metadata = described_class.new( + tools: tools, + authors: authors, + properties: properties + ) + metadata.timestamp = timestamp + metadata + end + + it 'has correct attributes' do + expect(metadata).to have_attributes( + tools: tools, + authors: authors, + properties: properties, + timestamp: timestamp + ) + end +end diff --git a/spec/lib/gitlab/ci/templates/MATLAB_spec.rb b/spec/lib/gitlab/ci/templates/MATLAB_spec.rb index 3889d1fc8c9..8b6ff7f27a2 100644 --- a/spec/lib/gitlab/ci/templates/MATLAB_spec.rb +++ b/spec/lib/gitlab/ci/templates/MATLAB_spec.rb @@ -20,7 +20,7 @@ RSpec.describe 'MATLAB.gitlab-ci.yml' do end it 'creates all jobs' do - expect(build_names).to include('command', 'test', 'test_artifacts') + expect(build_names).to include('command', 'test', 'test_artifacts', 'build') end end end diff --git a/spec/lib/gitlab/ci/trace/stream_spec.rb b/spec/lib/gitlab/ci/trace/stream_spec.rb index d65b6fb41f6..9439d29aa11 100644 --- a/spec/lib/gitlab/ci/trace/stream_spec.rb +++ b/spec/lib/gitlab/ci/trace/stream_spec.rb @@ -243,6 +243,56 @@ RSpec.describe Gitlab::Ci::Trace::Stream, :clean_gitlab_redis_cache do expect(result.encoding).to eq(Encoding.default_external) end end + + context 'limit max size' do + before do + # specifying BUFFER_SIZE forces to seek backwards + allow(described_class).to receive(:BUFFER_SIZE) + .and_return(2) + end + + it 'returns every lines with respect of the size' do + all_lines = lines.join + max_size = all_lines.bytesize.div(2) + result = stream.raw(max_size: max_size) + + expect(result.bytes).to eq(all_lines.bytes[-max_size..]) + expect(result.lines.count).to be > 1 + expect(result.encoding).to eq(Encoding.default_external) + end + + it 'returns everything if trying to get too many bytes' do + all_lines = lines.join + result = stream.raw(max_size: all_lines.bytesize * 2) + + expect(result).to eq(all_lines) + expect(result.encoding).to eq(Encoding.default_external) + end + end + + context 'limit max lines and max size' do + before do + # specifying BUFFER_SIZE forces to seek backwards + allow(described_class).to receive(:BUFFER_SIZE) + .and_return(2) + end + + it 'returns max lines if max size is greater' do + result = stream.raw(last_lines: 2, max_size: lines.join.bytesize * 2) + + expect(result).to eq(lines.last(2).join) + expect(result.encoding).to eq(Encoding.default_external) + end + + it 'returns max size if max lines is greater' do + all_lines = lines.join + max_size = all_lines.bytesize.div(2) + result = stream.raw(last_lines: lines.size * 2, max_size: max_size) + + expect(result.bytes).to eq(all_lines.bytes[-max_size..]) + expect(result.encoding).to eq(Encoding.default_external) + end + end end let(:path) { __FILE__ } diff --git a/spec/lib/gitlab/ci/variables/builder/pipeline_spec.rb b/spec/lib/gitlab/ci/variables/builder/pipeline_spec.rb index 0880c556523..860a1fd30bd 100644 --- a/spec/lib/gitlab/ci/variables/builder/pipeline_spec.rb +++ b/spec/lib/gitlab/ci/variables/builder/pipeline_spec.rb @@ -108,12 +108,17 @@ RSpec.describe Gitlab::Ci::Variables::Builder::Pipeline, feature_category: :secr 'CI_MERGE_REQUEST_SOURCE_PROJECT_URL' => merge_request.source_project.web_url, 'CI_MERGE_REQUEST_SOURCE_BRANCH_NAME' => merge_request.source_branch.to_s, 'CI_MERGE_REQUEST_SOURCE_BRANCH_SHA' => '', + 'CI_MERGE_REQUEST_SOURCE_BRANCH_PROTECTED' => ProtectedBranch.protected?( + merge_request.source_project, + merge_request.source_branch + ).to_s, 'CI_MERGE_REQUEST_TITLE' => merge_request.title, 'CI_MERGE_REQUEST_ASSIGNEES' => merge_request.assignee_username_list, 'CI_MERGE_REQUEST_MILESTONE' => milestone.title, 'CI_MERGE_REQUEST_LABELS' => labels.map(&:title).sort.join(','), 'CI_MERGE_REQUEST_EVENT_TYPE' => 'detached', - 'CI_OPEN_MERGE_REQUESTS' => merge_request.to_reference(full: true)) + 'CI_OPEN_MERGE_REQUESTS' => merge_request.to_reference(full: true)), + 'CI_MERGE_REQUEST_SQUASH_ON_MERGE' => merge_request.squash_on_merge?.to_s end it 'exposes diff variables' do diff --git a/spec/lib/gitlab/ci/variables/builder_spec.rb b/spec/lib/gitlab/ci/variables/builder_spec.rb index 3411426fcdb..af745c75f42 100644 --- a/spec/lib/gitlab/ci/variables/builder_spec.rb +++ b/spec/lib/gitlab/ci/variables/builder_spec.rb @@ -10,18 +10,27 @@ RSpec.describe Gitlab::Ci::Variables::Builder, :clean_gitlab_redis_cache, featur let_it_be(:user) { create(:user) } let_it_be_with_reload(:job) do create(:ci_build, + :with_deployment, name: 'rspec:test 1', pipeline: pipeline, user: user, yaml_variables: [{ key: 'YAML_VARIABLE', value: 'value' }], - environment: 'test' + environment: 'review/$CI_COMMIT_REF_NAME', + options: { + environment: { + name: 'review/$CI_COMMIT_REF_NAME', + action: 'prepare', + deployment_tier: 'testing', + url: 'https://gitlab.com' + } + } ) end let(:builder) { described_class.new(pipeline) } describe '#scoped_variables' do - let(:environment) { job.expanded_environment_name } + let(:environment_name) { job.expanded_environment_name } let(:dependencies) { true } let(:predefined_variables) do [ @@ -34,7 +43,13 @@ RSpec.describe Gitlab::Ci::Variables::Builder, :clean_gitlab_redis_cache, featur { key: 'CI_NODE_TOTAL', value: '1' }, { key: 'CI_ENVIRONMENT_NAME', - value: 'test' }, + value: 'review/master' }, + { key: 'CI_ENVIRONMENT_ACTION', + value: 'prepare' }, + { key: 'CI_ENVIRONMENT_TIER', + value: 'testing' }, + { key: 'CI_ENVIRONMENT_URL', + value: 'https://gitlab.com' }, { key: 'CI', value: 'true' }, { key: 'GITLAB_CI', @@ -150,7 +165,7 @@ RSpec.describe Gitlab::Ci::Variables::Builder, :clean_gitlab_redis_cache, featur ].map { |var| var.merge(public: true, masked: false) } end - subject { builder.scoped_variables(job, environment: environment, dependencies: dependencies) } + subject { builder.scoped_variables(job, environment: environment_name, dependencies: dependencies) } it { is_expected.to be_instance_of(Gitlab::Ci::Variables::Collection) } diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb index f8f1d71e773..c09c0b31e97 100644 --- a/spec/lib/gitlab/ci/yaml_processor_spec.rb +++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb @@ -794,6 +794,28 @@ module Gitlab it_behaves_like 'returns errors', 'test_job_1 has the following needs duplicated: test_job_2.' end + + context 'when needed job name is too long' do + let(:job_name) { 'a' * (::Ci::BuildNeed::MAX_JOB_NAME_LENGTH + 1) } + + let(:config) do + <<-EOYML + lint_job: + script: 'echo lint_job' + rules: + - if: $var == null + needs: [#{job_name}] + #{job_name}: + script: 'echo job' + EOYML + end + + it 'returns an error' do + expect(subject.errors).to include( + "lint_job job: need `#{job_name}` name is too long (maximum is #{::Ci::BuildNeed::MAX_JOB_NAME_LENGTH} characters)" + ) + end + end end context 'rule needs as hash' do @@ -2020,6 +2042,52 @@ module Gitlab end end + describe 'id_tokens' do + subject(:execute) { described_class.new(config).execute } + + let(:build) { execute.builds.first } + let(:id_tokens_vars) { { ID_TOKEN_1: { aud: 'http://gcp.com' } } } + let(:job_id_tokens_vars) { { ID_TOKEN_2: { aud: 'http://job.com' } } } + + context 'when defined on job level' do + let(:config) do + YAML.dump({ + rspec: { script: 'rspec', id_tokens: id_tokens_vars } + }) + end + + it 'returns defined id_tokens' do + expect(build[:id_tokens]).to eq(id_tokens_vars) + end + end + + context 'when defined as default' do + let(:config) do + YAML.dump({ + default: { id_tokens: id_tokens_vars }, + rspec: { script: 'rspec' } + }) + end + + it 'returns inherited by default id_tokens' do + expect(build[:id_tokens]).to eq(id_tokens_vars) + end + end + + context 'when defined as default and on job level' do + let(:config) do + YAML.dump({ + default: { id_tokens: id_tokens_vars }, + rspec: { script: 'rspec', id_tokens: job_id_tokens_vars } + }) + end + + it 'overrides default and returns defined on job level' do + expect(build[:id_tokens]).to eq(job_id_tokens_vars) + end + end + end + describe "Artifacts" do it "returns artifacts when defined" do config = YAML.dump( @@ -2553,6 +2621,60 @@ module Gitlab scheduling_type: :dag ) end + + context 'when expanded job name is too long' do + let(:parallel_job_name) { 'a' * ::Ci::BuildNeed::MAX_JOB_NAME_LENGTH } + let(:needs) { [parallel_job_name] } + + before do + config[parallel_job_name] = { stage: 'build', script: 'test', parallel: 1 } + end + + it 'returns an error' do + expect(subject.errors).to include( + "test1 job: need `#{parallel_job_name} 1/1` name is too long (maximum is #{::Ci::BuildNeed::MAX_JOB_NAME_LENGTH} characters)" + ) + end + end + + context 'when parallel job has matrix specified' do + let(:var1) { '1' } + let(:var2) { '2' } + + before do + config[:parallel] = { stage: 'build', script: 'test', parallel: { matrix: [{ VAR1: var1, VAR2: var2 }] } } + end + + it 'does create jobs with valid specification' do + expect(subject.builds.size).to eq(6) + expect(subject.builds[3]).to eq( + stage: 'test', + stage_idx: 2, + name: 'test1', + only: { refs: %w[branches tags] }, + options: { script: ['test'] }, + needs_attributes: [ + { name: 'parallel: [1, 2]', artifacts: true, optional: false } + ], + when: "on_success", + allow_failure: false, + job_variables: [], + root_variables_inheritance: true, + scheduling_type: :dag + ) + end + + context 'when expanded job name is too long' do + let(:var1) { '1' * (::Ci::BuildNeed::MAX_JOB_NAME_LENGTH / 2) } + let(:var2) { '2' * (::Ci::BuildNeed::MAX_JOB_NAME_LENGTH / 2) } + + it 'returns an error' do + expect(subject.errors).to include( + "test1 job: need `parallel: [#{var1}, #{var2}]` name is too long (maximum is #{::Ci::BuildNeed::MAX_JOB_NAME_LENGTH} characters)" + ) + end + end + end end context 'needs dependencies artifacts' do |