636 lines
		
	
	
		
			21 KiB
		
	
	
	
		
			Ruby
		
	
	
	
			
		
		
	
	
			636 lines
		
	
	
		
			21 KiB
		
	
	
	
		
			Ruby
		
	
	
	
# frozen_string_literal: true
 | 
						|
 | 
						|
require 'spec_helper'
 | 
						|
 | 
						|
RSpec.describe Gitlab::Ci::Variables::Collection, feature_category: :secrets_management do
 | 
						|
  describe '.fabricate' do
 | 
						|
    using RSpec::Parameterized::TableSyntax
 | 
						|
 | 
						|
    where do
 | 
						|
      {
 | 
						|
        "given an array of variables": {
 | 
						|
          input: [
 | 
						|
            { key: 'VAR1', value: 'value1' },
 | 
						|
            { key: 'VAR2', value: 'value2' }
 | 
						|
          ]
 | 
						|
        },
 | 
						|
        "given a hash of variables": {
 | 
						|
          input: { 'VAR1' => 'value1', 'VAR2' => 'value2' }
 | 
						|
        },
 | 
						|
        "given a proc that evaluates to an array": {
 | 
						|
          input: -> do
 | 
						|
            [
 | 
						|
              { key: 'VAR1', value: 'value1' },
 | 
						|
              { key: 'VAR2', value: 'value2' }
 | 
						|
            ]
 | 
						|
          end
 | 
						|
        },
 | 
						|
        "given a proc that evaluates to a hash": {
 | 
						|
          input: -> do
 | 
						|
            { 'VAR1' => 'value1', 'VAR2' => 'value2' }
 | 
						|
          end
 | 
						|
        },
 | 
						|
        "given a collection": {
 | 
						|
          input: Gitlab::Ci::Variables::Collection.new(
 | 
						|
            [
 | 
						|
              { key: 'VAR1', value: 'value1' },
 | 
						|
              { key: 'VAR2', value: 'value2' }
 | 
						|
            ]
 | 
						|
          )
 | 
						|
        }
 | 
						|
      }
 | 
						|
    end
 | 
						|
 | 
						|
    with_them do
 | 
						|
      subject(:collection) { Gitlab::Ci::Variables::Collection.fabricate(input) }
 | 
						|
 | 
						|
      it 'returns a collection' do
 | 
						|
        expect(collection).to be_a(Gitlab::Ci::Variables::Collection)
 | 
						|
        expect(collection.size).to eq(2)
 | 
						|
        expect(collection.map(&:key)).to contain_exactly('VAR1', 'VAR2')
 | 
						|
        expect(collection.map(&:value)).to contain_exactly('value1', 'value2')
 | 
						|
      end
 | 
						|
    end
 | 
						|
 | 
						|
    context 'when given an unrecognized type' do
 | 
						|
      it 'raises error' do
 | 
						|
        expect { described_class.fabricate(1) }.to raise_error(ArgumentError)
 | 
						|
      end
 | 
						|
    end
 | 
						|
  end
 | 
						|
 | 
						|
  describe '.new' do
 | 
						|
    it 'can be initialized with an array' do
 | 
						|
      variable = { key: 'VAR', value: 'value', public: true, masked: false }
 | 
						|
 | 
						|
      collection = described_class.new([variable])
 | 
						|
 | 
						|
      expect(collection.first.to_runner_variable).to eq variable
 | 
						|
    end
 | 
						|
 | 
						|
    it 'can be initialized without an argument' do
 | 
						|
      is_expected.to be_none
 | 
						|
    end
 | 
						|
  end
 | 
						|
 | 
						|
  describe '#append' do
 | 
						|
    it 'appends a hash' do
 | 
						|
      subject.append(key: 'VARIABLE', value: 'something')
 | 
						|
 | 
						|
      is_expected.to be_one
 | 
						|
    end
 | 
						|
 | 
						|
    it 'appends a Ci::Variable' do
 | 
						|
      subject.append(build(:ci_variable))
 | 
						|
 | 
						|
      is_expected.to be_one
 | 
						|
    end
 | 
						|
 | 
						|
    it 'appends an internal resource' do
 | 
						|
      collection = described_class.new([{ key: 'TEST', value: '1' }])
 | 
						|
 | 
						|
      subject.append(collection.first)
 | 
						|
 | 
						|
      is_expected.to be_one
 | 
						|
    end
 | 
						|
 | 
						|
    it 'returns self' do
 | 
						|
      expect(subject.append(key: 'VAR', value: 'test'))
 | 
						|
        .to eq subject
 | 
						|
    end
 | 
						|
  end
 | 
						|
 | 
						|
  describe '#compact' do
 | 
						|
    subject do
 | 
						|
      described_class.new
 | 
						|
        .append(key: 'STRING', value: 'string')
 | 
						|
        .append(key: 'NIL', value: nil)
 | 
						|
        .append(key: nil, value: 'string')
 | 
						|
    end
 | 
						|
 | 
						|
    it 'returns a new Collection instance', :aggregate_failures do
 | 
						|
      collection = subject.compact
 | 
						|
 | 
						|
      expect(collection).to be_an_instance_of(described_class)
 | 
						|
      expect(collection).not_to eql(subject)
 | 
						|
    end
 | 
						|
 | 
						|
    it 'rejects pair that has nil value', :aggregate_failures do
 | 
						|
      collection = subject.compact
 | 
						|
 | 
						|
      expect(collection).not_to include(key: 'NIL', value: nil, public: true)
 | 
						|
      expect(collection).to include(key: 'STRING', value: 'string', public: true)
 | 
						|
      expect(collection).to include(key: nil, value: 'string', public: true)
 | 
						|
    end
 | 
						|
  end
 | 
						|
 | 
						|
  describe '#concat' do
 | 
						|
    it 'appends all elements from an array' do
 | 
						|
      collection = described_class.new([{ key: 'VAR_1', value: '1' }])
 | 
						|
      variables = [{ key: 'VAR_2', value: '2' }, { key: 'VAR_3', value: '3' }]
 | 
						|
 | 
						|
      collection.concat(variables)
 | 
						|
 | 
						|
      expect(collection).to include(key: 'VAR_1', value: '1', public: true)
 | 
						|
      expect(collection).to include(key: 'VAR_2', value: '2', public: true)
 | 
						|
      expect(collection).to include(key: 'VAR_3', value: '3', public: true)
 | 
						|
    end
 | 
						|
 | 
						|
    it 'appends all elements from other collection' do
 | 
						|
      collection = described_class.new([{ key: 'VAR_1', value: '1' }])
 | 
						|
      additional = described_class.new([{ key: 'VAR_2', value: '2' },
 | 
						|
                                        { key: 'VAR_3', value: '3' }])
 | 
						|
 | 
						|
      collection.concat(additional)
 | 
						|
 | 
						|
      expect(collection).to include(key: 'VAR_1', value: '1', public: true)
 | 
						|
      expect(collection).to include(key: 'VAR_2', value: '2', public: true)
 | 
						|
      expect(collection).to include(key: 'VAR_3', value: '3', public: true)
 | 
						|
    end
 | 
						|
 | 
						|
    it 'does not concatenate resource if it undefined' do
 | 
						|
      collection = described_class.new([{ key: 'VAR_1', value: '1' }])
 | 
						|
 | 
						|
      collection.concat(nil)
 | 
						|
 | 
						|
      expect(collection).to be_one
 | 
						|
    end
 | 
						|
 | 
						|
    it 'returns self' do
 | 
						|
      expect(subject.concat([key: 'VAR', value: 'test']))
 | 
						|
        .to eq subject
 | 
						|
    end
 | 
						|
  end
 | 
						|
 | 
						|
  describe '#+' do
 | 
						|
    it 'makes it possible to combine with an array' do
 | 
						|
      collection = described_class.new([{ key: 'TEST', value: '1' }])
 | 
						|
      variables = [{ key: 'TEST', value: 'something' }]
 | 
						|
 | 
						|
      expect((collection + variables).count).to eq 2
 | 
						|
    end
 | 
						|
 | 
						|
    it 'makes it possible to combine with another collection' do
 | 
						|
      collection = described_class.new([{ key: 'TEST', value: '1' }])
 | 
						|
      other = described_class.new([{ key: 'TEST', value: '2' }])
 | 
						|
 | 
						|
      expect((collection + other).count).to eq 2
 | 
						|
    end
 | 
						|
  end
 | 
						|
 | 
						|
  describe '#[]' do
 | 
						|
    subject { described_class.new(variables)[var_name] }
 | 
						|
 | 
						|
    shared_examples 'an array access operator' do
 | 
						|
      context 'for a non-existent variable name' do
 | 
						|
        let(:var_name) { 'UNKNOWN_VAR' }
 | 
						|
 | 
						|
        it 'returns nil' do
 | 
						|
          is_expected.to be_nil
 | 
						|
        end
 | 
						|
      end
 | 
						|
 | 
						|
      context 'for an existent variable name' do
 | 
						|
        let(:var_name) { 'VAR' }
 | 
						|
 | 
						|
        it 'returns the last Item' do
 | 
						|
          is_expected.to be_an_instance_of(Gitlab::Ci::Variables::Collection::Item)
 | 
						|
          expect(subject.to_runner_variable).to eq(variables.last)
 | 
						|
        end
 | 
						|
      end
 | 
						|
    end
 | 
						|
 | 
						|
    context 'with variable key with single entry' do
 | 
						|
      let(:variables) do
 | 
						|
        [
 | 
						|
          { key: 'VAR', value: 'value', public: true, masked: false }
 | 
						|
        ]
 | 
						|
      end
 | 
						|
 | 
						|
      it_behaves_like 'an array access operator'
 | 
						|
    end
 | 
						|
 | 
						|
    context 'with variable key with multiple entries' do
 | 
						|
      let(:variables) do
 | 
						|
        [
 | 
						|
          { key: 'VAR', value: 'value', public: true, masked: false },
 | 
						|
          { key: 'VAR', value: 'override value', public: true, masked: false }
 | 
						|
        ]
 | 
						|
      end
 | 
						|
 | 
						|
      it_behaves_like 'an array access operator'
 | 
						|
    end
 | 
						|
  end
 | 
						|
 | 
						|
  describe '#all' do
 | 
						|
    subject { described_class.new(variables).all(var_name) }
 | 
						|
 | 
						|
    shared_examples 'a method returning all known variables or nil' do
 | 
						|
      context 'for a non-existent variable name' do
 | 
						|
        let(:var_name) { 'UNKNOWN_VAR' }
 | 
						|
 | 
						|
        it 'returns nil' do
 | 
						|
          is_expected.to be_nil
 | 
						|
        end
 | 
						|
      end
 | 
						|
 | 
						|
      context 'for an existing variable name' do
 | 
						|
        let(:var_name) { 'VAR' }
 | 
						|
 | 
						|
        it 'returns all expected Items' do
 | 
						|
          is_expected.to eq(expected_variables.map { |v| Gitlab::Ci::Variables::Collection::Item.fabricate(v) })
 | 
						|
        end
 | 
						|
      end
 | 
						|
    end
 | 
						|
 | 
						|
    context 'with variable key with single entry' do
 | 
						|
      let(:variables) do
 | 
						|
        [
 | 
						|
          { key: 'VAR', value: 'value', public: true, masked: false }
 | 
						|
        ]
 | 
						|
      end
 | 
						|
 | 
						|
      it_behaves_like 'a method returning all known variables or nil' do
 | 
						|
        let(:expected_variables) do
 | 
						|
          [
 | 
						|
            { key: 'VAR', value: 'value', public: true, masked: false }
 | 
						|
          ]
 | 
						|
        end
 | 
						|
      end
 | 
						|
    end
 | 
						|
 | 
						|
    context 'with variable key with multiple entries' do
 | 
						|
      let(:variables) do
 | 
						|
        [
 | 
						|
          { key: 'VAR', value: 'value', public: true, masked: false },
 | 
						|
          { key: 'VAR', value: 'override value', public: true, masked: false }
 | 
						|
        ]
 | 
						|
      end
 | 
						|
 | 
						|
      it_behaves_like 'a method returning all known variables or nil' do
 | 
						|
        let(:expected_variables) do
 | 
						|
          [
 | 
						|
            { key: 'VAR', value: 'value', public: true, masked: false },
 | 
						|
            { key: 'VAR', value: 'override value', public: true, masked: false }
 | 
						|
          ]
 | 
						|
        end
 | 
						|
      end
 | 
						|
    end
 | 
						|
  end
 | 
						|
 | 
						|
  describe '#size' do
 | 
						|
    it 'returns zero for empty collection' do
 | 
						|
      collection = described_class.new([])
 | 
						|
 | 
						|
      expect(collection.size).to eq(0)
 | 
						|
    end
 | 
						|
 | 
						|
    it 'returns 2 for collection with 2 variables' do
 | 
						|
      collection = described_class.new(
 | 
						|
        [
 | 
						|
          { key: 'VAR1', value: 'value', public: true, masked: false },
 | 
						|
          { key: 'VAR2', value: 'value', public: true, masked: false }
 | 
						|
        ])
 | 
						|
 | 
						|
      expect(collection.size).to eq(2)
 | 
						|
    end
 | 
						|
 | 
						|
    it 'returns 3 for collection with 2 duplicate variables' do
 | 
						|
      collection = described_class.new(
 | 
						|
        [
 | 
						|
          { key: 'VAR1', value: 'value', public: true, masked: false },
 | 
						|
          { key: 'VAR2', value: 'value', public: true, masked: false },
 | 
						|
          { key: 'VAR1', value: 'value', public: true, masked: false }
 | 
						|
        ])
 | 
						|
 | 
						|
      expect(collection.size).to eq(3)
 | 
						|
    end
 | 
						|
  end
 | 
						|
 | 
						|
  describe '#to_runner_variables' do
 | 
						|
    it 'creates an array of hashes in a runner-compatible format' do
 | 
						|
      collection = described_class.new([{ key: 'TEST', value: '1' }])
 | 
						|
 | 
						|
      expect(collection.to_runner_variables)
 | 
						|
        .to eq [{ key: 'TEST', value: '1', public: true, masked: false }]
 | 
						|
    end
 | 
						|
  end
 | 
						|
 | 
						|
  describe '#to_hash' do
 | 
						|
    it 'returns regular hash in valid order without duplicates' do
 | 
						|
      collection = described_class.new
 | 
						|
        .append(key: 'TEST1', value: 'test-1')
 | 
						|
        .append(key: 'TEST2', value: 'test-2')
 | 
						|
        .append(key: 'TEST1', value: 'test-3')
 | 
						|
 | 
						|
      expect(collection.to_hash).to eq('TEST1' => 'test-3',
 | 
						|
                                       'TEST2' => 'test-2')
 | 
						|
 | 
						|
      expect(collection.to_hash).to include(TEST1: 'test-3')
 | 
						|
      expect(collection.to_hash).not_to include(TEST1: 'test-1')
 | 
						|
    end
 | 
						|
  end
 | 
						|
 | 
						|
  describe '#reject' do
 | 
						|
    let(:collection) do
 | 
						|
      described_class.new
 | 
						|
        .append(key: 'CI_JOB_NAME', value: 'test-1')
 | 
						|
        .append(key: 'CI_BUILD_ID', value: '1')
 | 
						|
        .append(key: 'TEST1', value: 'test-3')
 | 
						|
    end
 | 
						|
 | 
						|
    subject { collection.reject { |var| var[:key] =~ /\ACI_(JOB|BUILD)/ } }
 | 
						|
 | 
						|
    it 'returns a Collection instance' do
 | 
						|
      is_expected.to be_an_instance_of(described_class)
 | 
						|
    end
 | 
						|
 | 
						|
    it 'returns correctly filtered Collection' do
 | 
						|
      comp = collection.to_runner_variables.reject { |var| var[:key] =~ /\ACI_(JOB|BUILD)/ }
 | 
						|
      expect(subject.to_runner_variables).to eq(comp)
 | 
						|
    end
 | 
						|
  end
 | 
						|
 | 
						|
  describe '#sort_and_expand_all' do
 | 
						|
    context 'table tests' do
 | 
						|
      using RSpec::Parameterized::TableSyntax
 | 
						|
 | 
						|
      where do
 | 
						|
        {
 | 
						|
          "empty array": {
 | 
						|
            variables: [],
 | 
						|
            keep_undefined: false,
 | 
						|
            result: []
 | 
						|
          },
 | 
						|
          "empty string": {
 | 
						|
            variables: [
 | 
						|
              { key: 'variable', value: '' }
 | 
						|
            ],
 | 
						|
            result: [
 | 
						|
              { key: 'variable', value: '' }
 | 
						|
            ]
 | 
						|
          },
 | 
						|
          "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 with expand_raw_refs: true (default)": {
 | 
						|
            variables: [
 | 
						|
              { key: 'variable1', value: 'value1' },
 | 
						|
              { key: 'raw_var', value: 'raw-$variable1', raw: true },
 | 
						|
              { key: 'nonraw_var', value: 'nonraw-$variable1' },
 | 
						|
              { key: 'variable2', value: '$raw_var and $nonraw_var' }
 | 
						|
            ],
 | 
						|
            keep_undefined: false,
 | 
						|
            result: [
 | 
						|
              { key: 'variable1', value: 'value1' },
 | 
						|
              { key: 'raw_var', value: 'raw-$variable1', raw: true },
 | 
						|
              { key: 'nonraw_var', value: 'nonraw-value1' },
 | 
						|
              { key: 'variable2', value: 'raw-$variable1 and nonraw-value1' }
 | 
						|
            ]
 | 
						|
          },
 | 
						|
          "complex expansions with raw variable with expand_raw_refs: false": {
 | 
						|
            variables: [
 | 
						|
              { key: 'variable1', value: 'value1' },
 | 
						|
              { key: 'raw_var', value: 'raw-$variable1', raw: true },
 | 
						|
              { key: 'nonraw_var', value: 'nonraw-$variable1' },
 | 
						|
              { key: 'variable2', value: '$raw_var and $nonraw_var' }
 | 
						|
            ],
 | 
						|
            keep_undefined: false,
 | 
						|
            expand_raw_refs: false,
 | 
						|
            result: [
 | 
						|
              { key: 'variable1', value: 'value1' },
 | 
						|
              { key: 'raw_var', value: 'raw-$variable1', raw: true },
 | 
						|
              { key: 'nonraw_var', value: 'nonraw-value1' },
 | 
						|
              { key: 'variable2', value: '$raw_var and nonraw-value1' }
 | 
						|
            ]
 | 
						|
          },
 | 
						|
          "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' }
 | 
						|
            ]
 | 
						|
          },
 | 
						|
          "file variables with expand_file_refs: true": {
 | 
						|
            variables: [
 | 
						|
              { key: 'file_var', value: 'secret content', file: true },
 | 
						|
              { key: 'variable1', value: 'var one' },
 | 
						|
              { key: 'variable2', value: 'var two $variable1 $file_var' }
 | 
						|
            ],
 | 
						|
            result: [
 | 
						|
              { key: 'file_var', value: 'secret content' },
 | 
						|
              { key: 'variable1', value: 'var one' },
 | 
						|
              { key: 'variable2', value: 'var two var one secret content' }
 | 
						|
            ]
 | 
						|
          },
 | 
						|
          "file variables with expand_file_refs: false": {
 | 
						|
            variables: [
 | 
						|
              { key: 'file_var', value: 'secret content', file: true },
 | 
						|
              { key: 'variable1', value: 'var one' },
 | 
						|
              { key: 'variable2', value: 'var two $variable1 $file_var' }
 | 
						|
            ],
 | 
						|
            expand_file_refs: false,
 | 
						|
            result: [
 | 
						|
              { key: 'file_var', value: 'secret content' },
 | 
						|
              { key: 'variable1', value: 'var one' },
 | 
						|
              { key: 'variable2', value: 'var two var one $file_var' }
 | 
						|
            ]
 | 
						|
          }
 | 
						|
        }
 | 
						|
      end
 | 
						|
 | 
						|
      with_them do
 | 
						|
        let(:collection) { Gitlab::Ci::Variables::Collection.new(variables) }
 | 
						|
        let(:options) do
 | 
						|
          { keep_undefined: keep_undefined,
 | 
						|
            expand_raw_refs: expand_raw_refs,
 | 
						|
            expand_file_refs: expand_file_refs }.compact
 | 
						|
        end
 | 
						|
 | 
						|
        subject(:expanded_result) { collection.sort_and_expand_all(**options) }
 | 
						|
 | 
						|
        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 'preserves raw attribute' do
 | 
						|
          expect(subject.pluck(:key, :raw).to_h).to eq(collection.pluck(:key, :raw).to_h)
 | 
						|
        end
 | 
						|
      end
 | 
						|
    end
 | 
						|
  end
 | 
						|
 | 
						|
  describe '#to_s' do
 | 
						|
    let(:variables) do
 | 
						|
      [
 | 
						|
        { key: 'VAR', value: 'value', public: true },
 | 
						|
        { key: 'VAR2', value: 'value2', public: false }
 | 
						|
      ]
 | 
						|
    end
 | 
						|
 | 
						|
    let(:errors) { 'circular variable reference detected' }
 | 
						|
    let(:collection) { described_class.new(variables, errors) }
 | 
						|
 | 
						|
    subject(:result) { collection.to_s }
 | 
						|
 | 
						|
    it { is_expected.to eq("[\"VAR\", \"VAR2\"], @errors='circular variable reference detected'") }
 | 
						|
  end
 | 
						|
end
 |