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