From 311b0269b4eb9839fa63f80c8d7a58f32b8138a0 Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Thu, 18 Nov 2021 13:16:36 +0000 Subject: Add latest changes from gitlab-org/gitlab@14-5-stable-ee --- spec/lib/gitlab/ci/artifact_file_reader_spec.rb | 11 - spec/lib/gitlab/ci/artifacts/metrics_spec.rb | 6 +- spec/lib/gitlab/ci/build/auto_retry_spec.rb | 2 + .../ci/build/rules/rule/clause/exists_spec.rb | 28 +- .../ci/config/entry/include/rules/rule_spec.rb | 16 +- .../lib/gitlab/ci/config/entry/processable_spec.rb | 8 + spec/lib/gitlab/ci/config/extendable_spec.rb | 44 ++ .../gitlab/ci/config/external/processor_spec.rb | 2 +- spec/lib/gitlab/ci/config/external/rules_spec.rb | 28 +- spec/lib/gitlab/ci/config_spec.rb | 90 +++- .../ci/pipeline/chain/validate/external_spec.rb | 2 +- .../gitlab/ci/pipeline/quota/deployments_spec.rb | 6 +- spec/lib/gitlab/ci/pipeline/seed/build_spec.rb | 56 +-- spec/lib/gitlab/ci/reports/security/report_spec.rb | 22 + .../lib/gitlab/ci/reports/security/reports_spec.rb | 21 +- .../templates/Jobs/deploy_gitlab_ci_yaml_spec.rb | 30 +- .../templates/Jobs/sast_iac_gitlab_ci_yaml_spec.rb | 65 +++ .../templates/auto_devops_gitlab_ci_yaml_spec.rb | 20 +- .../ci/templates/kaniko_gitlab_ci_yaml_spec.rb | 25 ++ .../terraform_latest_gitlab_ci_yaml_spec.rb | 2 +- spec/lib/gitlab/ci/trace/archive_spec.rb | 169 +++++--- spec/lib/gitlab/ci/trace/metrics_spec.rb | 18 +- spec/lib/gitlab/ci/trace_spec.rb | 10 - spec/lib/gitlab/ci/variables/builder_spec.rb | 38 ++ spec/lib/gitlab/ci/variables/collection_spec.rb | 482 +++++++++------------ spec/lib/gitlab/ci/yaml_processor_spec.rb | 58 +++ 26 files changed, 798 insertions(+), 461 deletions(-) create mode 100644 spec/lib/gitlab/ci/templates/Jobs/sast_iac_gitlab_ci_yaml_spec.rb create mode 100644 spec/lib/gitlab/ci/templates/kaniko_gitlab_ci_yaml_spec.rb create mode 100644 spec/lib/gitlab/ci/variables/builder_spec.rb (limited to 'spec/lib/gitlab/ci') diff --git a/spec/lib/gitlab/ci/artifact_file_reader_spec.rb b/spec/lib/gitlab/ci/artifact_file_reader_spec.rb index 83a37655ea9..e982f0eb015 100644 --- a/spec/lib/gitlab/ci/artifact_file_reader_spec.rb +++ b/spec/lib/gitlab/ci/artifact_file_reader_spec.rb @@ -18,17 +18,6 @@ RSpec.describe Gitlab::Ci::ArtifactFileReader do expect(YAML.safe_load(subject).keys).to contain_exactly('rspec', 'time', 'custom') end - context 'when FF ci_new_artifact_file_reader is disabled' do - before do - stub_feature_flags(ci_new_artifact_file_reader: false) - end - - it 'returns the content at the path' do - is_expected.to be_present - expect(YAML.safe_load(subject).keys).to contain_exactly('rspec', 'time', 'custom') - end - end - context 'when path does not exist' do let(:path) { 'file/does/not/exist.txt' } let(:expected_error) do diff --git a/spec/lib/gitlab/ci/artifacts/metrics_spec.rb b/spec/lib/gitlab/ci/artifacts/metrics_spec.rb index 3a2095498ec..0ce76285b03 100644 --- a/spec/lib/gitlab/ci/artifacts/metrics_spec.rb +++ b/spec/lib/gitlab/ci/artifacts/metrics_spec.rb @@ -10,9 +10,9 @@ RSpec.describe Gitlab::Ci::Artifacts::Metrics, :prometheus do let(:counter) { metrics.send(:destroyed_artifacts_counter) } it 'increments a single counter' do - subject.increment_destroyed_artifacts(10) - subject.increment_destroyed_artifacts(20) - subject.increment_destroyed_artifacts(30) + subject.increment_destroyed_artifacts_count(10) + subject.increment_destroyed_artifacts_count(20) + subject.increment_destroyed_artifacts_count(30) expect(counter.get).to eq 60 expect(counter.values.count).to eq 1 diff --git a/spec/lib/gitlab/ci/build/auto_retry_spec.rb b/spec/lib/gitlab/ci/build/auto_retry_spec.rb index fc5999d59ac..9ff9200322e 100644 --- a/spec/lib/gitlab/ci/build/auto_retry_spec.rb +++ b/spec/lib/gitlab/ci/build/auto_retry_spec.rb @@ -25,6 +25,8 @@ RSpec.describe Gitlab::Ci::Build::AutoRetry do "quota is exceeded" | 0 | { max: 2 } | :ci_quota_exceeded | false "no matching runner" | 0 | { max: 2 } | :no_matching_runner | false "missing dependencies" | 0 | { max: 2 } | :missing_dependency_failure | false + "forward deployment failure" | 0 | { max: 2 } | :forward_deployment_failure | false + "environment creation failure" | 0 | { max: 2 } | :environment_creation_failure | false end with_them do diff --git a/spec/lib/gitlab/ci/build/rules/rule/clause/exists_spec.rb b/spec/lib/gitlab/ci/build/rules/rule/clause/exists_spec.rb index 86dd5569a96..f192862c1c4 100644 --- a/spec/lib/gitlab/ci/build/rules/rule/clause/exists_spec.rb +++ b/spec/lib/gitlab/ci/build/rules/rule/clause/exists_spec.rb @@ -3,10 +3,8 @@ require 'spec_helper' RSpec.describe Gitlab::Ci::Build::Rules::Rule::Clause::Exists do - describe '#satisfied_by?' do - let(:pipeline) { build(:ci_pipeline, project: project, sha: project.repository.head_commit.sha) } - - subject { described_class.new(globs).satisfied_by?(pipeline, nil) } + shared_examples 'an exists rule with a context' do + subject { described_class.new(globs).satisfied_by?(pipeline, context) } it_behaves_like 'a glob matching rule' do let(:project) { create(:project, :custom_repo, files: files) } @@ -24,4 +22,26 @@ RSpec.describe Gitlab::Ci::Build::Rules::Rule::Clause::Exists do it { is_expected.to be_truthy } end end + + describe '#satisfied_by?' do + let(:pipeline) { build(:ci_pipeline, project: project, sha: project.repository.head_commit.sha) } + + context 'when context is Build::Context::Build' do + it_behaves_like 'an exists rule with a context' do + let(:context) { Gitlab::Ci::Build::Context::Build.new(pipeline, sha: 'abc1234') } + end + end + + context 'when context is Build::Context::Global' do + it_behaves_like 'an exists rule with a context' do + let(:context) { Gitlab::Ci::Build::Context::Global.new(pipeline, yaml_variables: {}) } + end + end + + context 'when context is Config::External::Context' do + it_behaves_like 'an exists rule with a context' do + let(:context) { Gitlab::Ci::Config::External::Context.new(project: project, sha: project.repository.tree.sha) } + end + 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 b99048e2c18..0505b17ea91 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 @@ -5,7 +5,7 @@ require 'fast_spec_helper' RSpec.describe Gitlab::Ci::Config::Entry::Include::Rules::Rule do let(:factory) do Gitlab::Config::Entry::Factory.new(described_class) - .value(config) + .value(config) end subject(:entry) { factory.create! } @@ -25,6 +25,12 @@ RSpec.describe Gitlab::Ci::Config::Entry::Include::Rules::Rule do it { is_expected.to be_valid } end + context 'when specifying an exists: clause' do + let(:config) { { exists: './this.md' } } + + it { is_expected.to be_valid } + end + context 'using a list of multiple expressions' do let(:config) { { if: ['$MY_VAR == "this"', '$YOUR_VAR == "that"'] } } @@ -86,5 +92,13 @@ RSpec.describe Gitlab::Ci::Config::Entry::Include::Rules::Rule do expect(subject).to eq(if: '$THIS || $THAT') end end + + context 'when specifying an exists: clause' do + let(:config) { { exists: './test.md' } } + + it 'returns the config' do + expect(subject).to eq(exists: './test.md') + end + 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 b872f6644a2..c9c28e2eb8b 100644 --- a/spec/lib/gitlab/ci/config/entry/processable_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/processable_spec.rb @@ -33,6 +33,14 @@ RSpec.describe Gitlab::Ci::Config::Entry::Processable do end end + context 'when job name is more than 255' do + let(:entry) { node_class.new(config, name: ('a' * 256).to_sym) } + + it 'shows a validation error' do + expect(entry.errors).to include "job name is too long (maximum is 255 characters)" + end + end + context 'when job name is empty' do let(:entry) { node_class.new(config, name: ''.to_sym) } diff --git a/spec/lib/gitlab/ci/config/extendable_spec.rb b/spec/lib/gitlab/ci/config/extendable_spec.rb index 481f55d790e..2fc009569fc 100644 --- a/spec/lib/gitlab/ci/config/extendable_spec.rb +++ b/spec/lib/gitlab/ci/config/extendable_spec.rb @@ -73,6 +73,50 @@ RSpec.describe Gitlab::Ci::Config::Extendable do end end + context 'when the job tries to delete an extension key' do + let(:hash) do + { + something: { + script: 'deploy', + only: { variables: %w[$SOMETHING] } + }, + + test1: { + extends: 'something', + script: 'ls', + only: {} + }, + + test2: { + extends: 'something', + script: 'ls', + only: nil + } + } + end + + it 'deletes the key if assigned to null' do + expect(subject.to_hash).to eq( + something: { + script: 'deploy', + only: { variables: %w[$SOMETHING] } + }, + test1: { + extends: 'something', + script: 'ls', + only: { + variables: %w[$SOMETHING] + } + }, + test2: { + extends: 'something', + script: 'ls', + only: nil + } + ) + end + end + context 'when a hash uses recursive extensions' do let(:hash) do { diff --git a/spec/lib/gitlab/ci/config/external/processor_spec.rb b/spec/lib/gitlab/ci/config/external/processor_spec.rb index c2f28253f54..2e9e6f95071 100644 --- a/spec/lib/gitlab/ci/config/external/processor_spec.rb +++ b/spec/lib/gitlab/ci/config/external/processor_spec.rb @@ -406,7 +406,7 @@ RSpec.describe Gitlab::Ci::Config::External::Processor do context 'when rules defined' do context 'when a rule is invalid' do let(:values) do - { include: [{ local: 'builds.yml', rules: [{ exists: ['$MY_VAR'] }] }] } + { include: [{ local: 'builds.yml', rules: [{ changes: ['$MY_VAR'] }] }] } end it 'raises IncludeError' do diff --git a/spec/lib/gitlab/ci/config/external/rules_spec.rb b/spec/lib/gitlab/ci/config/external/rules_spec.rb index 9a5c29befa2..1e42cb30ae7 100644 --- a/spec/lib/gitlab/ci/config/external/rules_spec.rb +++ b/spec/lib/gitlab/ci/config/external/rules_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'fast_spec_helper' +require 'spec_helper' RSpec.describe Gitlab::Ci::Config::External::Rules do let(:rule_hashes) {} @@ -32,6 +32,26 @@ RSpec.describe Gitlab::Ci::Config::External::Rules do end end + context 'when there is a rule with exists' do + let(:project) { create(:project, :repository) } + let(:context) { double(project: project, sha: project.repository.tree.sha, top_level_worktree_paths: ['test.md']) } + let(:rule_hashes) { [{ exists: 'Dockerfile' }] } + + context 'when the file does not exist' do + it { is_expected.to eq(false) } + end + + context 'when the file exists' do + let(:context) { double(project: project, sha: project.repository.tree.sha, top_level_worktree_paths: ['Dockerfile']) } + + before do + project.repository.create_file(project.owner, 'Dockerfile', "commit", message: 'test', branch_name: "master") + end + + it { is_expected.to eq(true) } + end + end + context 'when there is a rule with if and when' do let(:rule_hashes) { [{ if: '$MY_VAR == "hello"', when: 'on_success' }] } @@ -41,12 +61,12 @@ RSpec.describe Gitlab::Ci::Config::External::Rules do end end - context 'when there is a rule with exists' do - let(:rule_hashes) { [{ exists: ['$MY_VAR'] }] } + context 'when there is a rule with changes' do + let(:rule_hashes) { [{ changes: ['$MY_VAR'] }] } it 'raises an error' do expect { result }.to raise_error(described_class::InvalidIncludeRulesError, - 'invalid include rule: {:exists=>["$MY_VAR"]}') + 'invalid include rule: {:changes=>["$MY_VAR"]}') end end end diff --git a/spec/lib/gitlab/ci/config_spec.rb b/spec/lib/gitlab/ci/config_spec.rb index 3ec4519748f..1b3e8a2ce4a 100644 --- a/spec/lib/gitlab/ci/config_spec.rb +++ b/spec/lib/gitlab/ci/config_spec.rb @@ -14,7 +14,7 @@ RSpec.describe Gitlab::Ci::Config do end let(:config) do - described_class.new(yml, project: nil, sha: nil, user: nil) + described_class.new(yml, project: nil, pipeline: nil, sha: nil, user: nil) end context 'when config is valid' do @@ -286,9 +286,12 @@ RSpec.describe Gitlab::Ci::Config do end context "when using 'include' directive" do - let(:group) { create(:group) } + let_it_be(:group) { create(:group) } + let(:project) { create(:project, :repository, group: group) } let(:main_project) { create(:project, :repository, :public, group: group) } + let(:pipeline) { build(:ci_pipeline, project: project) } + let(:remote_location) { 'https://gitlab.com/gitlab-org/gitlab-foss/blob/1234/.gitlab-ci-1.yml' } let(:local_location) { 'spec/fixtures/gitlab/ci/external_files/.gitlab-ci-template-1.yml' } @@ -327,7 +330,7 @@ RSpec.describe Gitlab::Ci::Config do end let(:config) do - described_class.new(gitlab_ci_yml, project: project, sha: '12345', user: user) + described_class.new(gitlab_ci_yml, project: project, pipeline: pipeline, sha: '12345', user: user) end before do @@ -594,7 +597,7 @@ RSpec.describe Gitlab::Ci::Config do job1: { script: ["echo 'hello from main file'"], variables: { - VARIABLE_DEFINED_IN_MAIN_FILE: 'some value' + VARIABLE_DEFINED_IN_MAIN_FILE: 'some value' } } }) @@ -725,26 +728,91 @@ RSpec.describe Gitlab::Ci::Config do end context "when an 'include' has rules" do + context "when the rule is an if" do + let(:gitlab_ci_yml) do + <<~HEREDOC + include: + - local: #{local_location} + rules: + - if: $CI_PROJECT_ID == "#{project_id}" + image: ruby:2.7 + HEREDOC + end + + context 'when the rules condition is satisfied' do + let(:project_id) { project.id } + + it 'includes the file' do + expect(config.to_hash).to include(local_location_hash) + end + end + + context 'when the rules condition is satisfied' do + let(:project_id) { non_existing_record_id } + + it 'does not include the file' do + expect(config.to_hash).not_to include(local_location_hash) + end + end + end + + context "when the rule is an exists" do + let(:gitlab_ci_yml) do + <<~HEREDOC + include: + - local: #{local_location} + rules: + - exists: "#{filename}" + image: ruby:2.7 + HEREDOC + end + + before do + project.repository.create_file( + project.creator, + 'my_builds.yml', + local_file_content, + message: 'Add my_builds.yml', + branch_name: '12345' + ) + end + + context 'when the exists file does not exist' do + let(:filename) { 'not_a_real_file.md' } + + it 'does not include the file' do + expect(config.to_hash).not_to include(local_location_hash) + end + end + + context 'when the exists file does exist' do + let(:filename) { 'my_builds.yml' } + + it 'does include the file' do + expect(config.to_hash).to include(local_location_hash) + end + end + end + end + + context "when an 'include' has rules with a pipeline variable" do let(:gitlab_ci_yml) do <<~HEREDOC include: - local: #{local_location} rules: - - if: $CI_PROJECT_ID == "#{project_id}" - image: ruby:2.7 + - if: $CI_COMMIT_SHA == "#{project.commit.sha}" HEREDOC end - context 'when the rules condition is satisfied' do - let(:project_id) { project.id } - + context 'when a pipeline is passed' do it 'includes the file' do expect(config.to_hash).to include(local_location_hash) end end - context 'when the rules condition is satisfied' do - let(:project_id) { non_existing_record_id } + context 'when a pipeline is not passed' do + let(:pipeline) { nil } it 'does not include the file' do expect(config.to_hash).not_to include(local_location_hash) diff --git a/spec/lib/gitlab/ci/pipeline/chain/validate/external_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/validate/external_spec.rb index 16517b39a45..cf21c98dbd5 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/validate/external_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/validate/external_spec.rb @@ -83,7 +83,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Validate::External do end end - it 'respects the defined payload schema' do + it 'respects the defined payload schema', :saas do expect(::Gitlab::HTTP).to receive(:post) do |_url, params| expect(params[:body]).to match_schema('/external_validation') expect(params[:timeout]).to eq(described_class::DEFAULT_VALIDATION_REQUEST_TIMEOUT) diff --git a/spec/lib/gitlab/ci/pipeline/quota/deployments_spec.rb b/spec/lib/gitlab/ci/pipeline/quota/deployments_spec.rb index c52994fc6a2..5b0917c5c6f 100644 --- a/spec/lib/gitlab/ci/pipeline/quota/deployments_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/quota/deployments_spec.rb @@ -3,9 +3,9 @@ require 'spec_helper' RSpec.describe Gitlab::Ci::Pipeline::Quota::Deployments do - let_it_be(:namespace) { create(:namespace) } - let_it_be(:default_plan, reload: true) { create(:default_plan) } - let_it_be(:project, reload: true) { create(:project, :repository, namespace: namespace) } + let_it_be_with_refind(:namespace) { create(:namespace) } + let_it_be_with_reload(:default_plan) { create(:default_plan) } + let_it_be_with_reload(:project) { create(:project, :repository, namespace: namespace) } let_it_be(:plan_limits) { create(:plan_limits, plan: default_plan) } let(:pipeline) { build_stubbed(:ci_pipeline, project: project) } diff --git a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb index 3aa6b2e3c05..e2b64e65938 100644 --- a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do - let_it_be(:project) { create(:project, :repository) } + let_it_be_with_reload(:project) { create(:project, :repository) } let_it_be(:head_sha) { project.repository.head_commit.id } let(:pipeline) { build(:ci_empty_pipeline, project: project, sha: head_sha) } @@ -13,7 +13,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do let(:previous_stages) { [] } let(:current_stage) { double(seeds_names: [attributes[:name]]) } - let(:seed_build) { described_class.new(seed_context, attributes, previous_stages, current_stage) } + let(:seed_build) { described_class.new(seed_context, attributes, previous_stages + [current_stage]) } describe '#attributes' do subject { seed_build.attributes } @@ -393,12 +393,14 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do describe '#to_resource' do subject { seed_build.to_resource } - context 'when job is not a bridge' do + context 'when job is Ci::Build' do it { is_expected.to be_a(::Ci::Build) } it { is_expected.to be_valid } shared_examples_for 'deployment job' do it 'returns a job with deployment' do + expect { subject }.to change { Environment.count }.by(1) + expect(subject.deployment).not_to be_nil expect(subject.deployment.deployable).to eq(subject) expect(subject.deployment.environment.name).to eq(expected_environment_name) @@ -413,6 +415,8 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do shared_examples_for 'ensures environment existence' do it 'has environment' do + expect { subject }.to change { Environment.count }.by(1) + expect(subject).to be_has_environment expect(subject.environment).to eq(environment_name) expect(subject.metadata.expanded_environment_name).to eq(expected_environment_name) @@ -422,6 +426,8 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do shared_examples_for 'ensures environment inexistence' do it 'does not have environment' do + expect { subject }.not_to change { Environment.count } + expect(subject).not_to be_has_environment expect(subject.environment).to be_nil expect(subject.metadata&.expanded_environment_name).to be_nil @@ -1212,14 +1218,8 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do ] end - context 'when FF :variable_inside_variable is enabled' do - before do - stub_feature_flags(variable_inside_variable: [project]) - end - - it "does not have errors" do - expect(subject.errors).to be_empty - end + it "does not have errors" do + expect(subject.errors).to be_empty end end @@ -1232,36 +1232,20 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do ] end - context 'when FF :variable_inside_variable is disabled' do - before do - stub_feature_flags(variable_inside_variable: false) - end - - it "does not have errors" do - expect(subject.errors).to be_empty - end + it "returns an error" do + expect(subject.errors).to contain_exactly( + 'rspec: circular variable reference detected: ["A", "B", "C"]') end - context 'when FF :variable_inside_variable is enabled' do - before do - stub_feature_flags(variable_inside_variable: [project]) - end + context 'with job:rules:[if:]' do + let(:attributes) { { name: 'rspec', ref: 'master', rules: [{ if: '$C != null', when: 'always' }] } } - it "returns an error" do - expect(subject.errors).to contain_exactly( - 'rspec: circular variable reference detected: ["A", "B", "C"]') + it "included? does not raise" do + expect { subject.included? }.not_to raise_error end - context 'with job:rules:[if:]' do - let(:attributes) { { name: 'rspec', ref: 'master', rules: [{ if: '$C != null', when: 'always' }] } } - - it "included? does not raise" do - expect { subject.included? }.not_to raise_error - end - - it "included? returns true" do - expect(subject.included?).to eq(true) - end + it "included? returns true" do + expect(subject.included?).to eq(true) end 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 5a85c3f19fc..a8b962ee970 100644 --- a/spec/lib/gitlab/ci/reports/security/report_spec.rb +++ b/spec/lib/gitlab/ci/reports/security/report_spec.rb @@ -221,4 +221,26 @@ RSpec.describe Gitlab::Ci::Reports::Security::Report do end end end + + describe '#has_signatures?' do + let(:finding) { create(:ci_reports_security_finding, signatures: signatures) } + + subject { report.has_signatures? } + + before do + report.add_finding(finding) + end + + context 'when the findings of the report does not have signatures' do + let(:signatures) { [] } + + it { is_expected.to be_falsey } + end + + context 'when the findings of the report have signatures' do + let(:signatures) { [instance_double(Gitlab::Ci::Reports::Security::FindingSignature)] } + + it { is_expected.to be_truthy } + end + end end diff --git a/spec/lib/gitlab/ci/reports/security/reports_spec.rb b/spec/lib/gitlab/ci/reports/security/reports_spec.rb index 9b1e02f1418..79eee642552 100644 --- a/spec/lib/gitlab/ci/reports/security/reports_spec.rb +++ b/spec/lib/gitlab/ci/reports/security/reports_spec.rb @@ -54,11 +54,12 @@ RSpec.describe Gitlab::Ci::Reports::Security::Reports do end describe "#violates_default_policy_against?" do - let(:high_severity_dast) { build(:ci_reports_security_finding, severity: 'high', report_type: :dast) } + let(:high_severity_dast) { build(:ci_reports_security_finding, severity: 'high', report_type: 'dast') } let(:vulnerabilities_allowed) { 0 } let(:severity_levels) { %w(critical high) } + let(:vulnerability_states) { %w(newly_detected)} - subject { security_reports.violates_default_policy_against?(target_reports, vulnerabilities_allowed, severity_levels) } + subject { security_reports.violates_default_policy_against?(target_reports, vulnerabilities_allowed, severity_levels, vulnerability_states) } before do security_reports.get_report('sast', artifact).add_finding(high_severity_dast) @@ -108,6 +109,22 @@ RSpec.describe Gitlab::Ci::Reports::Security::Reports do it { is_expected.to be(false) } end + + context 'with related report_types' do + let(:report_types) { %w(dast sast) } + + subject { security_reports.violates_default_policy_against?(target_reports, vulnerabilities_allowed, severity_levels, vulnerability_states, report_types) } + + it { is_expected.to be(true) } + end + + context 'with unrelated report_types' do + let(:report_types) { %w(dependency_scanning sast) } + + subject { security_reports.violates_default_policy_against?(target_reports, vulnerabilities_allowed, severity_levels, vulnerability_states, report_types) } + + it { is_expected.to be(false) } + end end end end diff --git a/spec/lib/gitlab/ci/templates/Jobs/deploy_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/Jobs/deploy_gitlab_ci_yaml_spec.rb index d377cf0c735..789f694b4b4 100644 --- a/spec/lib/gitlab/ci/templates/Jobs/deploy_gitlab_ci_yaml_spec.rb +++ b/spec/lib/gitlab/ci/templates/Jobs/deploy_gitlab_ci_yaml_spec.rb @@ -27,9 +27,9 @@ RSpec.describe 'Jobs/Deploy.gitlab-ci.yml' do end describe 'the created pipeline' do - let(:project) { create(:project, :repository) } - let(:user) { project.owner } + let_it_be(:project, refind: true) { create(:project, :repository) } + let(:user) { project.owner } let(:default_branch) { 'master' } let(:pipeline_ref) { default_branch } let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_ref) } @@ -43,23 +43,23 @@ RSpec.describe 'Jobs/Deploy.gitlab-ci.yml' do allow(project).to receive(:default_branch).and_return(default_branch) end - context 'with no cluster' do + context 'with no cluster or agent' do it 'does not create any kubernetes deployment jobs' do expect(build_names).to eq %w(placeholder) end end context 'with only a disabled cluster' do - let!(:cluster) { create(:cluster, :project, :provided_by_gcp, enabled: false, projects: [project]) } + before do + create(:cluster, :project, :provided_by_gcp, enabled: false, projects: [project]) + end it 'does not create any kubernetes deployment jobs' do expect(build_names).to eq %w(placeholder) end end - context 'with an active cluster' do - let!(:cluster) { create(:cluster, :project, :provided_by_gcp, projects: [project]) } - + shared_examples_for 'pipeline with deployment jobs' do context 'on master' do it 'by default' do expect(build_names).to include('production') @@ -218,5 +218,21 @@ RSpec.describe 'Jobs/Deploy.gitlab-ci.yml' do end end end + + context 'with an agent' do + before do + create(:cluster_agent, project: project) + end + + it_behaves_like 'pipeline with deployment jobs' + end + + context 'with a cluster' do + before do + create(:cluster, :project, :provided_by_gcp, projects: [project]) + end + + it_behaves_like 'pipeline with deployment jobs' + end end end diff --git a/spec/lib/gitlab/ci/templates/Jobs/sast_iac_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/Jobs/sast_iac_gitlab_ci_yaml_spec.rb new file mode 100644 index 00000000000..b9256ece78b --- /dev/null +++ b/spec/lib/gitlab/ci/templates/Jobs/sast_iac_gitlab_ci_yaml_spec.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Jobs/SAST-IaC.latest.gitlab-ci.yml' do + subject(:template) { Gitlab::Template::GitlabCiYmlTemplate.find('Jobs/SAST-IaC.latest') } + + describe 'the created pipeline' do + let_it_be(:project) { create(:project, :repository) } + let_it_be(:user) { project.owner } + + let(:default_branch) { 'main' } + let(:pipeline_ref) { default_branch } + let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_ref) } + let(:pipeline) { service.execute!(:push).payload } + let(:build_names) { pipeline.builds.pluck(:name) } + + before do + stub_ci_pipeline_yaml_file(template.content) + allow_next_instance_of(Ci::BuildScheduleWorker) do |instance| + allow(instance).to receive(:perform).and_return(true) + end + allow(project).to receive(:default_branch).and_return(default_branch) + end + + context 'on feature branch' do + let(:pipeline_ref) { 'feature' } + + it 'creates the kics-iac-sast job' do + expect(build_names).to contain_exactly('kics-iac-sast') + end + end + + context 'on merge request' do + let(:service) { MergeRequests::CreatePipelineService.new(project: project, current_user: user) } + let(:merge_request) { create(:merge_request, :simple, source_project: project) } + let(:pipeline) { service.execute(merge_request).payload } + + it 'has no jobs' do + expect(pipeline).to be_merge_request_event + expect(build_names).to be_empty + end + end + + context 'SAST_DISABLED is set' do + before do + create(:ci_variable, key: 'SAST_DISABLED', value: 'true', project: project) + end + + context 'on default branch' do + it 'has no jobs' do + expect { pipeline }.to raise_error(Ci::CreatePipelineService::CreateError) + end + end + + context 'on feature branch' do + let(:pipeline_ref) { 'feature' } + + it 'has no jobs' do + expect { pipeline }.to raise_error(Ci::CreatePipelineService::CreateError) + end + end + end + end +end diff --git a/spec/lib/gitlab/ci/templates/auto_devops_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/auto_devops_gitlab_ci_yaml_spec.rb index 7602309627b..64ef6ecd7f8 100644 --- a/spec/lib/gitlab/ci/templates/auto_devops_gitlab_ci_yaml_spec.rb +++ b/spec/lib/gitlab/ci/templates/auto_devops_gitlab_ci_yaml_spec.rb @@ -148,9 +148,7 @@ RSpec.describe 'Auto-DevOps.gitlab-ci.yml' do it_behaves_like 'no Kubernetes deployment job' end - context 'when the project has an active cluster' do - let!(:cluster) { create(:cluster, :project, :provided_by_gcp, projects: [project]) } - + shared_examples 'pipeline with Kubernetes jobs' do describe 'deployment-related builds' do context 'on default branch' do it 'does not include rollout jobs besides production' do @@ -233,6 +231,22 @@ RSpec.describe 'Auto-DevOps.gitlab-ci.yml' do end end end + + context 'when a cluster is attached' do + before do + create(:cluster, :project, :provided_by_gcp, projects: [project]) + end + + it_behaves_like 'pipeline with Kubernetes jobs' + end + + context 'when project has an Agent is present' do + before do + create(:cluster_agent, project: project) + end + + it_behaves_like 'pipeline with Kubernetes jobs' + end end describe 'buildpack detection' do diff --git a/spec/lib/gitlab/ci/templates/kaniko_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/kaniko_gitlab_ci_yaml_spec.rb new file mode 100644 index 00000000000..c7dbbea4622 --- /dev/null +++ b/spec/lib/gitlab/ci/templates/kaniko_gitlab_ci_yaml_spec.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Kaniko.gitlab-ci.yml' do + subject(:template) { Gitlab::Template::GitlabCiYmlTemplate.find('Kaniko') } + + describe 'the created pipeline' do + let(:pipeline_branch) { 'master' } + let(:project) { create(:project, :custom_repo, files: { 'Dockerfile' => 'FROM alpine:latest' }) } + let(:user) { project.owner } + let(:service) { Ci::CreatePipelineService.new(project, user, ref: pipeline_branch ) } + let(:pipeline) { service.execute!(:push).payload } + let(:build_names) { pipeline.builds.pluck(:name) } + + before do + stub_ci_pipeline_yaml_file(template.content) + allow(Ci::BuildScheduleWorker).to receive(:perform).and_return(true) + end + + it 'creates "kaniko-build" job' do + expect(build_names).to include('kaniko-build') + end + end +end diff --git a/spec/lib/gitlab/ci/templates/terraform_latest_gitlab_ci_yaml_spec.rb b/spec/lib/gitlab/ci/templates/terraform_latest_gitlab_ci_yaml_spec.rb index 3d1306e82a5..fd5d5d6af7f 100644 --- a/spec/lib/gitlab/ci/templates/terraform_latest_gitlab_ci_yaml_spec.rb +++ b/spec/lib/gitlab/ci/templates/terraform_latest_gitlab_ci_yaml_spec.rb @@ -27,7 +27,7 @@ RSpec.describe 'Terraform.latest.gitlab-ci.yml' do context 'on master branch' do it 'creates init, validate and build jobs', :aggregate_failures do expect(pipeline.errors).to be_empty - expect(build_names).to include('init', 'validate', 'build', 'deploy') + expect(build_names).to include('validate', 'build', 'deploy') end end diff --git a/spec/lib/gitlab/ci/trace/archive_spec.rb b/spec/lib/gitlab/ci/trace/archive_spec.rb index c9fc4e720c4..5e965f94347 100644 --- a/spec/lib/gitlab/ci/trace/archive_spec.rb +++ b/spec/lib/gitlab/ci/trace/archive_spec.rb @@ -3,99 +3,134 @@ require 'spec_helper' RSpec.describe Gitlab::Ci::Trace::Archive do - let_it_be(:job) { create(:ci_build, :success, :trace_live) } - let_it_be_with_reload(:trace_metadata) { create(:ci_build_trace_metadata, build: job) } - let_it_be(:src_checksum) do - job.trace.read { |stream| Digest::MD5.hexdigest(stream.raw) } - end - - let(:metrics) { spy('metrics') } - - describe '#execute' do - subject { described_class.new(job, trace_metadata, metrics) } - - it 'computes and assigns checksum' do - Gitlab::Ci::Trace::ChunkedIO.new(job) do |stream| - expect { subject.execute!(stream) }.to change { Ci::JobArtifact.count }.by(1) - end - - expect(trace_metadata.checksum).to eq(src_checksum) - expect(trace_metadata.trace_artifact).to eq(job.job_artifacts_trace) + context 'with transactional fixtures' do + let_it_be(:job) { create(:ci_build, :success, :trace_live) } + let_it_be_with_reload(:trace_metadata) { create(:ci_build_trace_metadata, build: job) } + let_it_be(:src_checksum) do + job.trace.read { |stream| Digest::MD5.hexdigest(stream.raw) } end - context 'validating artifact checksum' do - let(:trace) { 'abc' } - let(:stream) { StringIO.new(trace, 'rb') } - let(:src_checksum) { Digest::MD5.hexdigest(trace) } + let(:metrics) { spy('metrics') } - context 'when the object store is disabled' do - before do - stub_artifacts_object_storage(enabled: false) - end - - it 'skips validation' do - subject.execute!(stream) + describe '#execute' do + subject { described_class.new(job, trace_metadata, metrics) } - expect(trace_metadata.checksum).to eq(src_checksum) - expect(trace_metadata.remote_checksum).to be_nil - expect(metrics) - .not_to have_received(:increment_error_counter) - .with(type: :archive_invalid_checksum) + it 'computes and assigns checksum' do + Gitlab::Ci::Trace::ChunkedIO.new(job) do |stream| + expect { subject.execute!(stream) }.to change { Ci::JobArtifact.count }.by(1) end + + expect(trace_metadata.checksum).to eq(src_checksum) + expect(trace_metadata.trace_artifact).to eq(job.job_artifacts_trace) end - context 'with background_upload enabled' do - before do - stub_artifacts_object_storage(background_upload: true) - end + context 'validating artifact checksum' do + let(:trace) { 'abc' } + let(:stream) { StringIO.new(trace, 'rb') } + let(:src_checksum) { Digest::MD5.hexdigest(trace) } - it 'skips validation' do - subject.execute!(stream) + context 'when the object store is disabled' do + before do + stub_artifacts_object_storage(enabled: false) + end - expect(trace_metadata.checksum).to eq(src_checksum) - expect(trace_metadata.remote_checksum).to be_nil - expect(metrics) - .not_to have_received(:increment_error_counter) - .with(type: :archive_invalid_checksum) + it 'skips validation' do + subject.execute!(stream) + expect(trace_metadata.checksum).to eq(src_checksum) + expect(trace_metadata.remote_checksum).to be_nil + expect(metrics) + .not_to have_received(:increment_error_counter) + .with(error_reason: :archive_invalid_checksum) + end end - end - context 'with direct_upload enabled' do - before do - stub_artifacts_object_storage(direct_upload: true) - end + context 'with background_upload enabled' do + before do + stub_artifacts_object_storage(background_upload: true) + end - it 'validates the archived trace' do - subject.execute!(stream) + it 'skips validation' do + subject.execute!(stream) - expect(trace_metadata.checksum).to eq(src_checksum) - expect(trace_metadata.remote_checksum).to eq(src_checksum) - expect(metrics) - .not_to have_received(:increment_error_counter) - .with(type: :archive_invalid_checksum) + expect(trace_metadata.checksum).to eq(src_checksum) + expect(trace_metadata.remote_checksum).to be_nil + expect(metrics) + .not_to have_received(:increment_error_counter) + .with(error_reason: :archive_invalid_checksum) + end end - context 'when the checksum does not match' do - let(:invalid_remote_checksum) { SecureRandom.hex } - + context 'with direct_upload enabled' do before do - expect(::Gitlab::Ci::Trace::RemoteChecksum) - .to receive(:new) - .with(an_instance_of(Ci::JobArtifact)) - .and_return(double(md5_checksum: invalid_remote_checksum)) + stub_artifacts_object_storage(direct_upload: true) end it 'validates the archived trace' do subject.execute!(stream) expect(trace_metadata.checksum).to eq(src_checksum) - expect(trace_metadata.remote_checksum).to eq(invalid_remote_checksum) + expect(trace_metadata.remote_checksum).to eq(src_checksum) expect(metrics) - .to have_received(:increment_error_counter) - .with(type: :archive_invalid_checksum) + .not_to have_received(:increment_error_counter) + .with(error_reason: :archive_invalid_checksum) + end + + context 'when the checksum does not match' do + let(:invalid_remote_checksum) { SecureRandom.hex } + + before do + expect(::Gitlab::Ci::Trace::RemoteChecksum) + .to receive(:new) + .with(an_instance_of(Ci::JobArtifact)) + .and_return(double(md5_checksum: invalid_remote_checksum)) + end + + it 'validates the archived trace' do + subject.execute!(stream) + + expect(trace_metadata.checksum).to eq(src_checksum) + expect(trace_metadata.remote_checksum).to eq(invalid_remote_checksum) + expect(metrics) + .to have_received(:increment_error_counter) + .with(error_reason: :archive_invalid_checksum) + end end end end end end + + context 'without transactional fixtures', :delete do + let(:job) { create(:ci_build, :success, :trace_live) } + let(:trace_metadata) { create(:ci_build_trace_metadata, build: job) } + let(:stream) { StringIO.new('abc', 'rb') } + + describe '#execute!' do + subject(:execute) do + ::Gitlab::Ci::Trace::Archive.new(job, trace_metadata).execute!(stream) + end + + before do + stub_artifacts_object_storage(direct_upload: true) + end + + it 'does not upload the trace inside a database transaction', :delete do + expect(Ci::ApplicationRecord.connection.transaction_open?).to be_falsey + + allow_next_instance_of(Ci::JobArtifact) do |artifact| + artifact.job_id = job.id + + expect(artifact) + .to receive(:store_file!) + .and_wrap_original do |store_method, *args| + expect(Ci::ApplicationRecord.connection.transaction_open?).to be_falsey + + store_method.call(*args) + end + end + + execute + end + end + end end diff --git a/spec/lib/gitlab/ci/trace/metrics_spec.rb b/spec/lib/gitlab/ci/trace/metrics_spec.rb index 53e55a57973..733ffbbea22 100644 --- a/spec/lib/gitlab/ci/trace/metrics_spec.rb +++ b/spec/lib/gitlab/ci/trace/metrics_spec.rb @@ -17,23 +17,23 @@ RSpec.describe Gitlab::Ci::Trace::Metrics, :prometheus do end describe '#increment_error_counter' do - context 'when the operation type is known' do + context 'when the error reason is known' do it 'increments the counter' do - subject.increment_error_counter(type: :chunks_invalid_size) - subject.increment_error_counter(type: :chunks_invalid_checksum) - subject.increment_error_counter(type: :archive_invalid_checksum) + subject.increment_error_counter(error_reason: :chunks_invalid_size) + subject.increment_error_counter(error_reason: :chunks_invalid_checksum) + subject.increment_error_counter(error_reason: :archive_invalid_checksum) - expect(described_class.trace_errors_counter.get(type: :chunks_invalid_size)).to eq 1 - expect(described_class.trace_errors_counter.get(type: :chunks_invalid_checksum)).to eq 1 - expect(described_class.trace_errors_counter.get(type: :archive_invalid_checksum)).to eq 1 + expect(described_class.trace_errors_counter.get(error_reason: :chunks_invalid_size)).to eq 1 + expect(described_class.trace_errors_counter.get(error_reason: :chunks_invalid_checksum)).to eq 1 + expect(described_class.trace_errors_counter.get(error_reason: :archive_invalid_checksum)).to eq 1 expect(described_class.trace_errors_counter.values.count).to eq 3 end end - context 'when the operation type is known' do + context 'when the error reason is unknown' do it 'raises an exception' do - expect { subject.increment_error_counter(type: :invalid_type) } + expect { subject.increment_error_counter(error_reason: :invalid_type) } .to raise_error(ArgumentError) end end diff --git a/spec/lib/gitlab/ci/trace_spec.rb b/spec/lib/gitlab/ci/trace_spec.rb index 1a31b2dad56..888ceb7ff9a 100644 --- a/spec/lib/gitlab/ci/trace_spec.rb +++ b/spec/lib/gitlab/ci/trace_spec.rb @@ -25,16 +25,6 @@ RSpec.describe Gitlab::Ci::Trace, :clean_gitlab_redis_shared_state, factory_defa artifact1.file.migrate!(ObjectStorage::Store::REMOTE) end - it 'reloads the trace after is it migrated' do - stub_const('Gitlab::HttpIO::BUFFER_SIZE', test_data.length) - - expect_next_instance_of(Gitlab::HttpIO) do |http_io| - expect(http_io).to receive(:get_chunk).and_return(test_data, "") - end - - expect(artifact2.job.trace.raw).to eq(test_data) - end - it 'reloads the trace in case of a chunk error' do chunk_error = described_class::ChunkedIO::FailedToGetChunkError diff --git a/spec/lib/gitlab/ci/variables/builder_spec.rb b/spec/lib/gitlab/ci/variables/builder_spec.rb new file mode 100644 index 00000000000..10275f33484 --- /dev/null +++ b/spec/lib/gitlab/ci/variables/builder_spec.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Ci::Variables::Builder do + let(:builder) { described_class.new(pipeline) } + let(:pipeline) { create(:ci_pipeline) } + let(:job) { create(:ci_build, pipeline: pipeline) } + + describe '#scoped_variables' do + let(:environment) { job.expanded_environment_name } + let(:dependencies) { true } + + subject { builder.scoped_variables(job, environment: environment, dependencies: dependencies) } + + it 'returns the expected variables' do + keys = %w[CI_JOB_NAME + CI_JOB_STAGE + CI_NODE_TOTAL + CI_BUILD_NAME + CI_BUILD_STAGE] + + subject.map { |env| env[:key] }.tap do |names| + expect(names).to include(*keys) + end + end + + context 'feature flag disabled' do + before do + stub_feature_flags(ci_predefined_vars_in_builder: false) + end + + it 'returns no variables' do + expect(subject.map { |env| env[:key] }).to be_empty + end + end + end +end diff --git a/spec/lib/gitlab/ci/variables/collection_spec.rb b/spec/lib/gitlab/ci/variables/collection_spec.rb index 7ba98380986..26c560565e0 100644 --- a/spec/lib/gitlab/ci/variables/collection_spec.rb +++ b/spec/lib/gitlab/ci/variables/collection_spec.rb @@ -358,302 +358,210 @@ RSpec.describe Gitlab::Ci::Variables::Collection do end describe '#sort_and_expand_all' do - context 'when FF :variable_inside_variable is disabled' do - let_it_be(:project_with_flag_disabled) { create(:project) } - let_it_be(:project_with_flag_enabled) { create(:project) } - - before do - stub_feature_flags(variable_inside_variable: [project_with_flag_enabled]) - end + context 'table tests' do + using RSpec::Parameterized::TableSyntax - context 'table tests' do - using RSpec::Parameterized::TableSyntax - - where do - { - "empty array": { - variables: [], - keep_undefined: false - }, - "simple expansions": { - variables: [ - { key: 'variable', value: 'value' }, - { key: 'variable2', value: 'result' }, - { key: 'variable3', value: 'key$variable$variable2' } - ], - keep_undefined: false - }, - "complex expansion": { - variables: [ - { key: 'variable', value: 'value' }, - { key: 'variable2', value: 'key${variable}' } - ], - keep_undefined: false - }, - "out-of-order variable reference": { - variables: [ - { key: 'variable2', value: 'key${variable}' }, - { key: 'variable', value: 'value' } - ], - keep_undefined: false - }, - "complex expansions with raw variable": { - variables: [ - { key: 'variable3', value: 'key_${variable}_${variable2}' }, - { key: 'variable', value: '$variable2', raw: true }, - { key: 'variable2', value: 'value2' } - ], - keep_undefined: false - }, - "escaped characters in complex expansions are kept intact": { - variables: [ - { key: 'variable3', value: 'key_${variable}_$${HOME}_%%HOME%%' }, - { key: 'variable', value: '$variable2' }, - { key: 'variable2', value: 'value2' } - ], - keep_undefined: false - }, - "array with cyclic dependency": { - variables: [ - { key: 'variable', value: '$variable2' }, - { key: 'variable2', value: '$variable3' }, - { key: 'variable3', value: 'key$variable$variable2' } - ], - keep_undefined: true - } + where do + { + "empty array": { + variables: [], + keep_undefined: false, + result: [] + }, + "simple expansions": { + variables: [ + { key: 'variable', value: 'value' }, + { key: 'variable2', value: 'result' }, + { key: 'variable3', value: 'key$variable$variable2' }, + { key: 'variable4', value: 'key$variable$variable3' } + ], + keep_undefined: false, + result: [ + { key: 'variable', value: 'value' }, + { key: 'variable2', value: 'result' }, + { key: 'variable3', value: 'keyvalueresult' }, + { key: 'variable4', value: 'keyvaluekeyvalueresult' } + ] + }, + "complex expansion": { + variables: [ + { key: 'variable', value: 'value' }, + { key: 'variable2', value: 'key${variable}' } + ], + keep_undefined: false, + result: [ + { key: 'variable', value: 'value' }, + { key: 'variable2', value: 'keyvalue' } + ] + }, + "unused variables": { + variables: [ + { key: 'variable', value: 'value' }, + { key: 'variable2', value: 'result2' }, + { key: 'variable3', value: 'result3' }, + { key: 'variable4', value: 'key$variable$variable3' } + ], + keep_undefined: false, + result: [ + { key: 'variable', value: 'value' }, + { key: 'variable2', value: 'result2' }, + { key: 'variable3', value: 'result3' }, + { key: 'variable4', value: 'keyvalueresult3' } + ] + }, + "complex expansions": { + variables: [ + { key: 'variable', value: 'value' }, + { key: 'variable2', value: 'result' }, + { key: 'variable3', value: 'key${variable}${variable2}' } + ], + keep_undefined: false, + result: [ + { key: 'variable', value: 'value' }, + { key: 'variable2', value: 'result' }, + { key: 'variable3', value: 'keyvalueresult' } + ] + }, + "escaped characters in complex expansions keeping undefined are kept intact": { + variables: [ + { key: 'variable3', value: 'key_${variable}_$${HOME}_%%HOME%%' }, + { key: 'variable', value: '$variable2' }, + { key: 'variable2', value: 'value' } + ], + keep_undefined: true, + result: [ + { key: 'variable', value: 'value' }, + { key: 'variable2', value: 'value' }, + { key: 'variable3', value: 'key_value_$${HOME}_%%HOME%%' } + ] + }, + "escaped characters in complex expansions discarding undefined are kept intact": { + variables: [ + { key: 'variable2', value: 'key_${variable4}_$${HOME}_%%HOME%%' }, + { key: 'variable', value: 'value_$${HOME}_%%HOME%%' } + ], + keep_undefined: false, + result: [ + { key: 'variable', value: 'value_$${HOME}_%%HOME%%' }, + { key: 'variable2', value: 'key__$${HOME}_%%HOME%%' } + ] + }, + "out-of-order expansion": { + variables: [ + { key: 'variable3', value: 'key$variable2$variable' }, + { key: 'variable', value: 'value' }, + { key: 'variable2', value: 'result' } + ], + keep_undefined: false, + result: [ + { key: 'variable2', value: 'result' }, + { key: 'variable', value: 'value' }, + { key: 'variable3', value: 'keyresultvalue' } + ] + }, + "out-of-order complex expansion": { + variables: [ + { key: 'variable', value: 'value' }, + { key: 'variable2', value: 'result' }, + { key: 'variable3', value: 'key${variable2}${variable}' } + ], + keep_undefined: false, + result: [ + { key: 'variable', value: 'value' }, + { key: 'variable2', value: 'result' }, + { key: 'variable3', value: 'keyresultvalue' } + ] + }, + "missing variable discarding original": { + variables: [ + { key: 'variable2', value: 'key$variable' } + ], + keep_undefined: false, + result: [ + { key: 'variable2', value: 'key' } + ] + }, + "missing variable keeping original": { + variables: [ + { key: 'variable2', value: 'key$variable' } + ], + keep_undefined: true, + result: [ + { key: 'variable2', value: 'key$variable' } + ] + }, + "complex expansions with missing variable keeping original": { + variables: [ + { key: 'variable4', value: 'key${variable}${variable2}${variable3}' }, + { key: 'variable', value: 'value' }, + { key: 'variable3', value: 'value3' } + ], + keep_undefined: true, + result: [ + { key: 'variable', value: 'value' }, + { key: 'variable3', value: 'value3' }, + { key: 'variable4', value: 'keyvalue${variable2}value3' } + ] + }, + "complex expansions with raw variable": { + variables: [ + { key: 'variable3', value: 'key_${variable}_${variable2}' }, + { key: 'variable', value: '$variable2', raw: true }, + { key: 'variable2', value: 'value2' } + ], + keep_undefined: false, + result: [ + { key: 'variable', value: '$variable2', raw: true }, + { key: 'variable2', value: 'value2' }, + { key: 'variable3', value: 'key_$variable2_value2' } + ] + }, + "variable value referencing password with special characters": { + variables: [ + { key: 'VAR', value: '$PASSWORD' }, + { key: 'PASSWORD', value: 'my_password$$_%%_$A' }, + { key: 'A', value: 'value' } + ], + keep_undefined: false, + result: [ + { key: 'VAR', value: 'my_password$$_%%_value' }, + { key: 'PASSWORD', value: 'my_password$$_%%_value' }, + { key: 'A', value: 'value' } + ] + }, + "cyclic dependency causes original array to be returned": { + variables: [ + { key: 'variable', value: '$variable2' }, + { key: 'variable2', value: '$variable3' }, + { key: 'variable3', value: 'key$variable$variable2' } + ], + keep_undefined: false, + result: [ + { key: 'variable', value: '$variable2' }, + { key: 'variable2', value: '$variable3' }, + { key: 'variable3', value: 'key$variable$variable2' } + ] } - end - - with_them do - let(:collection) { Gitlab::Ci::Variables::Collection.new(variables, keep_undefined: keep_undefined) } - - subject { collection.sort_and_expand_all(project_with_flag_disabled) } - - it 'returns Collection' do - is_expected.to be_an_instance_of(Gitlab::Ci::Variables::Collection) - end - - it 'does not expand variables' do - var_hash = variables.pluck(:key, :value).to_h - expect(subject.to_hash).to eq(var_hash) - end - end + } end - end - context 'when FF :variable_inside_variable is enabled' do - let_it_be(:project_with_flag_disabled) { create(:project) } - let_it_be(:project_with_flag_enabled) { create(:project) } + with_them do + let(:collection) { Gitlab::Ci::Variables::Collection.new(variables) } - before do - stub_feature_flags(variable_inside_variable: [project_with_flag_enabled]) - end + subject { collection.sort_and_expand_all(keep_undefined: keep_undefined) } - context 'table tests' do - using RSpec::Parameterized::TableSyntax - - where do - { - "empty array": { - variables: [], - keep_undefined: false, - result: [] - }, - "simple expansions": { - variables: [ - { key: 'variable', value: 'value' }, - { key: 'variable2', value: 'result' }, - { key: 'variable3', value: 'key$variable$variable2' }, - { key: 'variable4', value: 'key$variable$variable3' } - ], - keep_undefined: false, - result: [ - { key: 'variable', value: 'value' }, - { key: 'variable2', value: 'result' }, - { key: 'variable3', value: 'keyvalueresult' }, - { key: 'variable4', value: 'keyvaluekeyvalueresult' } - ] - }, - "complex expansion": { - variables: [ - { key: 'variable', value: 'value' }, - { key: 'variable2', value: 'key${variable}' } - ], - keep_undefined: false, - result: [ - { key: 'variable', value: 'value' }, - { key: 'variable2', value: 'keyvalue' } - ] - }, - "unused variables": { - variables: [ - { key: 'variable', value: 'value' }, - { key: 'variable2', value: 'result2' }, - { key: 'variable3', value: 'result3' }, - { key: 'variable4', value: 'key$variable$variable3' } - ], - keep_undefined: false, - result: [ - { key: 'variable', value: 'value' }, - { key: 'variable2', value: 'result2' }, - { key: 'variable3', value: 'result3' }, - { key: 'variable4', value: 'keyvalueresult3' } - ] - }, - "complex expansions": { - variables: [ - { key: 'variable', value: 'value' }, - { key: 'variable2', value: 'result' }, - { key: 'variable3', value: 'key${variable}${variable2}' } - ], - keep_undefined: false, - result: [ - { key: 'variable', value: 'value' }, - { key: 'variable2', value: 'result' }, - { key: 'variable3', value: 'keyvalueresult' } - ] - }, - "escaped characters in complex expansions keeping undefined are kept intact": { - variables: [ - { key: 'variable3', value: 'key_${variable}_$${HOME}_%%HOME%%' }, - { key: 'variable', value: '$variable2' }, - { key: 'variable2', value: 'value' } - ], - keep_undefined: true, - result: [ - { key: 'variable', value: 'value' }, - { key: 'variable2', value: 'value' }, - { key: 'variable3', value: 'key_value_$${HOME}_%%HOME%%' } - ] - }, - "escaped characters in complex expansions discarding undefined are kept intact": { - variables: [ - { key: 'variable2', value: 'key_${variable4}_$${HOME}_%%HOME%%' }, - { key: 'variable', value: 'value_$${HOME}_%%HOME%%' } - ], - keep_undefined: false, - result: [ - { key: 'variable', value: 'value_$${HOME}_%%HOME%%' }, - { key: 'variable2', value: 'key__$${HOME}_%%HOME%%' } - ] - }, - "out-of-order expansion": { - variables: [ - { key: 'variable3', value: 'key$variable2$variable' }, - { key: 'variable', value: 'value' }, - { key: 'variable2', value: 'result' } - ], - keep_undefined: false, - result: [ - { key: 'variable2', value: 'result' }, - { key: 'variable', value: 'value' }, - { key: 'variable3', value: 'keyresultvalue' } - ] - }, - "out-of-order complex expansion": { - variables: [ - { key: 'variable', value: 'value' }, - { key: 'variable2', value: 'result' }, - { key: 'variable3', value: 'key${variable2}${variable}' } - ], - keep_undefined: false, - result: [ - { key: 'variable', value: 'value' }, - { key: 'variable2', value: 'result' }, - { key: 'variable3', value: 'keyresultvalue' } - ] - }, - "missing variable discarding original": { - variables: [ - { key: 'variable2', value: 'key$variable' } - ], - keep_undefined: false, - result: [ - { key: 'variable2', value: 'key' } - ] - }, - "missing variable keeping original": { - variables: [ - { key: 'variable2', value: 'key$variable' } - ], - keep_undefined: true, - result: [ - { key: 'variable2', value: 'key$variable' } - ] - }, - "complex expansions with missing variable keeping original": { - variables: [ - { key: 'variable4', value: 'key${variable}${variable2}${variable3}' }, - { key: 'variable', value: 'value' }, - { key: 'variable3', value: 'value3' } - ], - keep_undefined: true, - result: [ - { key: 'variable', value: 'value' }, - { key: 'variable3', value: 'value3' }, - { key: 'variable4', value: 'keyvalue${variable2}value3' } - ] - }, - "complex expansions with raw variable": { - variables: [ - { key: 'variable3', value: 'key_${variable}_${variable2}' }, - { key: 'variable', value: '$variable2', raw: true }, - { key: 'variable2', value: 'value2' } - ], - keep_undefined: false, - result: [ - { key: 'variable', value: '$variable2', raw: true }, - { key: 'variable2', value: 'value2' }, - { key: 'variable3', value: 'key_$variable2_value2' } - ] - }, - "variable value referencing password with special characters": { - variables: [ - { key: 'VAR', value: '$PASSWORD' }, - { key: 'PASSWORD', value: 'my_password$$_%%_$A' }, - { key: 'A', value: 'value' } - ], - keep_undefined: false, - result: [ - { key: 'VAR', value: 'my_password$$_%%_value' }, - { key: 'PASSWORD', value: 'my_password$$_%%_value' }, - { key: 'A', value: 'value' } - ] - }, - "cyclic dependency causes original array to be returned": { - variables: [ - { key: 'variable', value: '$variable2' }, - { key: 'variable2', value: '$variable3' }, - { key: 'variable3', value: 'key$variable$variable2' } - ], - keep_undefined: false, - result: [ - { key: 'variable', value: '$variable2' }, - { key: 'variable2', value: '$variable3' }, - { key: 'variable3', value: 'key$variable$variable2' } - ] - } - } + it 'returns Collection' do + is_expected.to be_an_instance_of(Gitlab::Ci::Variables::Collection) end - with_them do - let(:collection) { Gitlab::Ci::Variables::Collection.new(variables) } - - subject { collection.sort_and_expand_all(project_with_flag_enabled, keep_undefined: keep_undefined) } - - it 'returns Collection' do - is_expected.to be_an_instance_of(Gitlab::Ci::Variables::Collection) - end - - it 'expands variables' do - var_hash = result.to_h { |env| [env.fetch(:key), env.fetch(:value)] } - .with_indifferent_access - expect(subject.to_hash).to eq(var_hash) - end + it 'expands variables' do + var_hash = result.to_h { |env| [env.fetch(:key), env.fetch(:value)] } + .with_indifferent_access + expect(subject.to_hash).to eq(var_hash) + end - it 'preserves raw attribute' do - expect(subject.pluck(:key, :raw).to_h).to eq(collection.pluck(:key, :raw).to_h) - end + it 'preserves raw attribute' do + expect(subject.pluck(:key, :raw).to_h).to eq(collection.pluck(:key, :raw).to_h) end end end diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb index 1591c2e6b60..f00a801286d 100644 --- a/spec/lib/gitlab/ci/yaml_processor_spec.rb +++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb @@ -1046,6 +1046,64 @@ module Gitlab end end + context 'when overriding `extends`' do + let(:config) do + <<~YAML + .base: + script: test + variables: + VAR1: base var 1 + + test1: + extends: .base + variables: + VAR1: test1 var 1 + VAR2: test2 var 2 + + test2: + extends: .base + variables: + VAR2: test2 var 2 + + test3: + extends: .base + variables: {} + + test4: + extends: .base + variables: null + YAML + end + + it 'correctly extends jobs' do + expect(config_processor.builds[0]).to include( + name: 'test1', + options: { script: ['test'] }, + job_variables: [{ key: 'VAR1', value: 'test1 var 1', public: true }, + { key: 'VAR2', value: 'test2 var 2', public: true }] + ) + + expect(config_processor.builds[1]).to include( + name: 'test2', + options: { script: ['test'] }, + job_variables: [{ key: 'VAR1', value: 'base var 1', public: true }, + { key: 'VAR2', value: 'test2 var 2', public: true }] + ) + + expect(config_processor.builds[2]).to include( + name: 'test3', + options: { script: ['test'] }, + job_variables: [{ key: 'VAR1', value: 'base var 1', public: true }] + ) + + expect(config_processor.builds[3]).to include( + name: 'test4', + options: { script: ['test'] }, + job_variables: [] + ) + end + end + context 'when using recursive `extends`' do let(:config) do <<~YAML -- cgit v1.2.3