1# frozen_string_literal: true 2 3require 'spec_helper' 4 5RSpec.describe Ci::ParseDotenvArtifactService do 6 let_it_be(:project) { create(:project) } 7 let_it_be(:pipeline) { create(:ci_pipeline, project: project) } 8 9 let(:build) { create(:ci_build, pipeline: pipeline, project: project) } 10 let(:service) { described_class.new(project, nil) } 11 12 describe '#execute' do 13 subject { service.execute(artifact) } 14 15 context 'when build has a dotenv artifact' do 16 let!(:artifact) { create(:ci_job_artifact, :dotenv, job: build) } 17 18 it 'parses the artifact' do 19 expect(subject[:status]).to eq(:success) 20 21 expect(build.job_variables.as_json).to contain_exactly( 22 hash_including('key' => 'KEY1', 'value' => 'VAR1'), 23 hash_including('key' => 'KEY2', 'value' => 'VAR2')) 24 end 25 26 context 'when dotenv variables are conflicting against manual variables' do 27 before do 28 create(:ci_job_variable, job: build, key: 'KEY1') 29 end 30 31 it 'returns an error message that there is a duplicate variable' do 32 subject 33 34 expect(subject[:status]).to eq(:error) 35 expect(subject[:message]).to include("Key (key, job_id)=(KEY1, #{build.id}) already exists.") 36 expect(subject[:http_status]).to eq(:bad_request) 37 end 38 end 39 40 context 'when dotenv variables have duplicate variables' do 41 let!(:artifact) { create(:ci_job_artifact, :dotenv, job: build) } 42 let(:blob) do 43 <<~EOS 44 KEY1=VAR1 45 KEY2=VAR2 46 KEY2=VAR3 47 KEY1=VAR4 48 EOS 49 end 50 51 before do 52 allow(artifact).to receive(:each_blob).and_yield(blob) 53 end 54 55 it 'latest values get used' do 56 subject 57 58 expect(subject[:status]).to eq(:success) 59 60 expect(build.job_variables.as_json).to contain_exactly( 61 hash_including('key' => 'KEY1', 'value' => 'VAR4'), 62 hash_including('key' => 'KEY2', 'value' => 'VAR3')) 63 end 64 end 65 66 context 'when parse error happens' do 67 before do 68 allow(service).to receive(:scan_line!) { raise described_class::ParserError, 'Invalid Format' } 69 end 70 71 it 'returns error' do 72 expect(Gitlab::ErrorTracking).to receive(:track_exception) 73 .with(described_class::ParserError, job_id: build.id) 74 75 expect(subject[:status]).to eq(:error) 76 expect(subject[:message]).to eq('Invalid Format') 77 expect(subject[:http_status]).to eq(:bad_request) 78 end 79 end 80 81 context 'when artifact size is too big' do 82 before do 83 allow(artifact.file).to receive(:size) { 10.kilobytes } 84 end 85 86 it 'returns error' do 87 expect(subject[:status]).to eq(:error) 88 expect(subject[:message]).to eq("Dotenv Artifact Too Big. Maximum Allowable Size: #{service.send(:dotenv_size_limit)}") 89 expect(subject[:http_status]).to eq(:bad_request) 90 end 91 end 92 93 context 'when artifact has the specified blob' do 94 before do 95 allow(artifact).to receive(:each_blob).and_yield(blob) 96 end 97 98 context 'when a white space trails the key' do 99 let(:blob) { 'KEY1 =VAR1' } 100 101 it 'trims the trailing space' do 102 subject 103 104 expect(build.job_variables.as_json).to contain_exactly( 105 hash_including('key' => 'KEY1', 'value' => 'VAR1')) 106 end 107 end 108 109 context 'when multiple key/value pairs exist in one line' do 110 let(:blob) { 'KEY=VARCONTAINING=EQLS' } 111 112 it 'parses the dotenv data' do 113 subject 114 115 expect(build.job_variables.as_json).to contain_exactly( 116 hash_including('key' => 'KEY', 'value' => 'VARCONTAINING=EQLS')) 117 end 118 end 119 120 context 'when key contains UNICODE' do 121 let(:blob) { '=skateboard' } 122 123 it 'returns error' do 124 expect(subject[:status]).to eq(:error) 125 expect(subject[:message]).to eq("Validation failed: Key can contain only letters, digits and '_'.") 126 expect(subject[:http_status]).to eq(:bad_request) 127 end 128 end 129 130 context 'when value contains UNICODE' do 131 let(:blob) { 'skateboard=' } 132 133 it 'parses the dotenv data' do 134 subject 135 136 expect(build.job_variables.as_json).to contain_exactly( 137 hash_including('key' => 'skateboard', 'value' => '')) 138 end 139 end 140 141 context 'when key contains a space' do 142 let(:blob) { 'K E Y 1=VAR1' } 143 144 it 'returns error' do 145 expect(subject[:status]).to eq(:error) 146 expect(subject[:message]).to eq("Validation failed: Key can contain only letters, digits and '_'.") 147 expect(subject[:http_status]).to eq(:bad_request) 148 end 149 end 150 151 context 'when value contains a space' do 152 let(:blob) { 'KEY1=V A R 1' } 153 154 it 'parses the dotenv data' do 155 subject 156 157 expect(build.job_variables.as_json).to contain_exactly( 158 hash_including('key' => 'KEY1', 'value' => 'V A R 1')) 159 end 160 end 161 162 context 'when value is double quoated' do 163 let(:blob) { 'KEY1="VAR1"' } 164 165 it 'parses the value as-is' do 166 subject 167 168 expect(build.job_variables.as_json).to contain_exactly( 169 hash_including('key' => 'KEY1', 'value' => '"VAR1"')) 170 end 171 end 172 173 context 'when value is single quoated' do 174 let(:blob) { "KEY1='VAR1'" } 175 176 it 'parses the value as-is' do 177 subject 178 179 expect(build.job_variables.as_json).to contain_exactly( 180 hash_including('key' => 'KEY1', 'value' => "'VAR1'")) 181 end 182 end 183 184 context 'when value has white spaces in double quote' do 185 let(:blob) { 'KEY1=" VAR1 "' } 186 187 it 'parses the value as-is' do 188 subject 189 190 expect(build.job_variables.as_json).to contain_exactly( 191 hash_including('key' => 'KEY1', 'value' => '" VAR1 "')) 192 end 193 end 194 195 context 'when key is missing' do 196 let(:blob) { '=VAR1' } 197 198 it 'returns error' do 199 expect(subject[:status]).to eq(:error) 200 expect(subject[:message]).to match(/Key can't be blank/) 201 expect(subject[:http_status]).to eq(:bad_request) 202 end 203 end 204 205 context 'when value is missing' do 206 let(:blob) { 'KEY1=' } 207 208 it 'parses the dotenv data' do 209 subject 210 211 expect(build.job_variables.as_json).to contain_exactly( 212 hash_including('key' => 'KEY1', 'value' => '')) 213 end 214 end 215 216 context 'when it is not dotenv format' do 217 let(:blob) { "{ 'KEY1': 'VAR1' }" } 218 219 it 'returns error' do 220 expect(subject[:status]).to eq(:error) 221 expect(subject[:message]).to eq('Invalid Format') 222 expect(subject[:http_status]).to eq(:bad_request) 223 end 224 end 225 226 context 'when more than limitated variables are specified in dotenv' do 227 let(:blob) do 228 StringIO.new.tap do |s| 229 (service.send(:dotenv_variable_limit) + 1).times do |i| 230 s << "KEY#{i}=VAR#{i}\n" 231 end 232 end.string 233 end 234 235 it 'returns error' do 236 expect(subject[:status]).to eq(:error) 237 expect(subject[:message]).to eq("Dotenv files cannot have more than #{service.send(:dotenv_variable_limit)} variables") 238 expect(subject[:http_status]).to eq(:bad_request) 239 end 240 end 241 242 context 'when variables are cross-referenced in dotenv' do 243 let(:blob) do 244 <<~EOS 245 KEY1=VAR1 246 KEY2=${KEY1}_Test 247 EOS 248 end 249 250 it 'does not support variable expansion in dotenv parser' do 251 subject 252 253 expect(build.job_variables.as_json).to contain_exactly( 254 hash_including('key' => 'KEY1', 'value' => 'VAR1'), 255 hash_including('key' => 'KEY2', 'value' => '${KEY1}_Test')) 256 end 257 end 258 259 context 'when there is an empty line' do 260 let(:blob) do 261 <<~EOS 262 KEY1=VAR1 263 264 KEY2=VAR2 265 EOS 266 end 267 268 it 'does not support empty line in dotenv parser' do 269 subject 270 271 expect(subject[:status]).to eq(:error) 272 expect(subject[:message]).to eq('Invalid Format') 273 expect(subject[:http_status]).to eq(:bad_request) 274 end 275 end 276 277 context 'when there is a comment' do 278 let(:blob) do 279 <<~EOS 280 KEY1=VAR1 # This is variable 281 EOS 282 end 283 284 it 'does not support comment in dotenv parser' do 285 subject 286 287 expect(build.job_variables.as_json).to contain_exactly( 288 hash_including('key' => 'KEY1', 'value' => 'VAR1 # This is variable')) 289 end 290 end 291 end 292 end 293 294 context 'when build does not have a dotenv artifact' do 295 let!(:artifact) { } 296 297 it 'raises an error' do 298 expect { subject }.to raise_error(ArgumentError) 299 end 300 end 301 end 302end 303