fig 2.0.0.pre.alpha.4 → 2.0.0.pre.alpha.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/lib/fig/operating_system.rb +3 -0
  3. data/lib/fig/protocol/artifactory.rb +389 -0
  4. data/lib/fig/spec_utils.rb +312 -0
  5. data/lib/fig/version.rb +1 -1
  6. data/spec/application_configuration_spec.rb +73 -0
  7. data/spec/command/clean_spec.rb +62 -0
  8. data/spec/command/command_line_vs_package_spec.rb +32 -0
  9. data/spec/command/dump_package_definition_spec.rb +104 -0
  10. data/spec/command/environment_variables_spec.rb +62 -0
  11. data/spec/command/grammar_asset_spec.rb +391 -0
  12. data/spec/command/grammar_command_spec.rb +88 -0
  13. data/spec/command/grammar_environment_variable_spec.rb +384 -0
  14. data/spec/command/grammar_retrieve_spec.rb +74 -0
  15. data/spec/command/grammar_spec.rb +87 -0
  16. data/spec/command/grammar_spec_helper.rb +23 -0
  17. data/spec/command/include_file_spec.rb +73 -0
  18. data/spec/command/listing_spec.rb +1574 -0
  19. data/spec/command/miscellaneous_spec.rb +145 -0
  20. data/spec/command/publish_local_and_updates_spec.rb +32 -0
  21. data/spec/command/publishing_retrieval_spec.rb +423 -0
  22. data/spec/command/publishing_spec.rb +596 -0
  23. data/spec/command/running_commands_spec.rb +354 -0
  24. data/spec/command/suppress_includes_spec.rb +65 -0
  25. data/spec/command/suppress_warning_include_statement_missing_version_spec.rb +134 -0
  26. data/spec/command/update_lock_response_spec.rb +47 -0
  27. data/spec/command/usage_errors_spec.rb +481 -0
  28. data/spec/command_options_spec.rb +184 -0
  29. data/spec/command_spec.rb +49 -0
  30. data/spec/deparser/v1_spec.rb +64 -0
  31. data/spec/environment_variables_spec.rb +91 -0
  32. data/spec/figrc_spec.rb +144 -0
  33. data/spec/parser_spec.rb +398 -0
  34. data/spec/protocol/artifactory_spec.rb +599 -0
  35. data/spec/repository_spec.rb +117 -0
  36. data/spec/runtime_environment_spec.rb +357 -0
  37. data/spec/spec_helper.rb +1 -0
  38. data/spec/split_repo_url_spec.rb +190 -0
  39. data/spec/statement/asset_spec.rb +203 -0
  40. data/spec/statement/configuration_spec.rb +41 -0
  41. data/spec/support/formatters/seed_spitter.rb +12 -0
  42. data/spec/working_directory_maintainer_spec.rb +102 -0
  43. metadata +72 -5
@@ -0,0 +1,599 @@
1
+ # coding: utf-8
2
+
3
+ require 'spec_helper'
4
+ require 'fig/protocol/artifactory'
5
+
6
+ describe Fig::Protocol::Artifactory do
7
+ let(:artifactory) { Fig::Protocol::Artifactory.new }
8
+ let(:base_url) { URI('https://artifacts.example.com/artifactory/ui/api/v1/ui/v2/nativeBrowser/repo-name/') }
9
+
10
+ describe '#get_all_artifactory_entries' do
11
+ let(:mock_client) { double('Artifactory::Client') }
12
+ let(:base_url) { URI('https://artifacts.example.com/artifactory/ui/api/v1/ui/v2/nativeBrowser/repo-name/') }
13
+
14
+ before do
15
+ # Stub netrc authentication
16
+ allow(artifactory).to receive(:get_authentication_for).and_return(nil)
17
+ end
18
+
19
+ context 'when all entries fit in single page' do
20
+ it 'returns entries without pagination' do
21
+ response = {
22
+ 'data' => [
23
+ { 'name' => 'package1', 'folder' => true },
24
+ { 'name' => 'package2', 'folder' => true }
25
+ ],
26
+ 'continueState' => -1
27
+ }
28
+
29
+ expect(mock_client).to receive(:get)
30
+ .with(URI("https://artifacts.example.com/artifactory/ui/api/v1/ui/v2/nativeBrowser/repo-name/?recordNum=#{Fig::Protocol::Artifactory::INITIAL_LIST_FETCH_SIZE}"))
31
+ .and_return(response)
32
+
33
+ result = artifactory.send(:get_all_artifactory_entries, base_url, mock_client)
34
+ expect(result).to eq(response['data'])
35
+ end
36
+ end
37
+
38
+ context 'when entries require pagination' do
39
+ it 'follows pagination until continueState is -1' do
40
+ first_response = {
41
+ 'data' => [
42
+ { 'name' => 'package1', 'folder' => true },
43
+ { 'name' => 'package2', 'folder' => true }
44
+ ],
45
+ 'continueState' => 'cursor123'
46
+ }
47
+
48
+ final_response = {
49
+ 'data' => [
50
+ { 'name' => 'package1', 'folder' => true },
51
+ { 'name' => 'package2', 'folder' => true },
52
+ { 'name' => 'package3', 'folder' => true }
53
+ ],
54
+ 'continueState' => -1
55
+ }
56
+
57
+ expect(mock_client).to receive(:get)
58
+ .with(URI("https://artifacts.example.com/artifactory/ui/api/v1/ui/v2/nativeBrowser/repo-name/?recordNum=#{Fig::Protocol::Artifactory::INITIAL_LIST_FETCH_SIZE}"))
59
+ .and_return(first_response)
60
+
61
+ expect(mock_client).to receive(:get)
62
+ .with(URI('https://artifacts.example.com/artifactory/ui/api/v1/ui/v2/nativeBrowser/repo-name/?recordNum=cursor123'))
63
+ .and_return(final_response)
64
+
65
+ result = artifactory.send(:get_all_artifactory_entries, base_url, mock_client)
66
+ expect(result).to eq(final_response['data'])
67
+ end
68
+ end
69
+
70
+ context 'when continueState is nil' do
71
+ it 'returns entries and stops pagination' do
72
+ response = {
73
+ 'data' => [{ 'name' => 'package1', 'folder' => true }],
74
+ 'continueState' => nil
75
+ }
76
+
77
+ expect(mock_client).to receive(:get)
78
+ .with(URI("https://artifacts.example.com/artifactory/ui/api/v1/ui/v2/nativeBrowser/repo-name/?recordNum=#{Fig::Protocol::Artifactory::INITIAL_LIST_FETCH_SIZE}"))
79
+ .and_return(response)
80
+
81
+ result = artifactory.send(:get_all_artifactory_entries, base_url, mock_client)
82
+ expect(result).to eq(response['data'])
83
+ end
84
+ end
85
+
86
+ context 'when FIG_ARTIFACTORY_PAGESIZE is set' do
87
+ it 'uses custom page size' do
88
+ allow(ENV).to receive(:[]).with('FIG_ARTIFACTORY_PAGESIZE').and_return('5000')
89
+
90
+ response = {
91
+ 'data' => [{ 'name' => 'package1', 'folder' => true }],
92
+ 'continueState' => -1
93
+ }
94
+
95
+ expect(mock_client).to receive(:get)
96
+ .with(URI('https://artifacts.example.com/artifactory/ui/api/v1/ui/v2/nativeBrowser/repo-name/?recordNum=5000'))
97
+ .and_return(response)
98
+
99
+ result = artifactory.send(:get_all_artifactory_entries, base_url, mock_client)
100
+ expect(result).to eq(response['data'])
101
+ end
102
+ end
103
+
104
+ context 'when response has no data field' do
105
+ it 'returns empty array' do
106
+ response = { 'continueState' => -1 }
107
+
108
+ expect(mock_client).to receive(:get)
109
+ .with(URI("https://artifacts.example.com/artifactory/ui/api/v1/ui/v2/nativeBrowser/repo-name/?recordNum=#{Fig::Protocol::Artifactory::INITIAL_LIST_FETCH_SIZE}"))
110
+ .and_return(response)
111
+
112
+ result = artifactory.send(:get_all_artifactory_entries, base_url, mock_client)
113
+ expect(result).to eq([])
114
+ end
115
+ end
116
+ end
117
+
118
+ describe '#download_list' do
119
+ let(:uri) { URI('art://artifacts.example.com/artifactory/repo-name/') }
120
+ let(:artifactory) { Fig::Protocol::Artifactory.new }
121
+ let(:mock_client) { double('Artifactory::Client') }
122
+
123
+ before do
124
+ allow(::Artifactory::Client).to receive(:new).and_return(mock_client)
125
+ # Stub netrc authentication
126
+ allow(artifactory).to receive(:get_authentication_for).and_return(nil)
127
+ end
128
+
129
+ context 'when repository has packages and versions' do
130
+ it 'returns sorted package/version strings' do
131
+ # Mock package listing
132
+ packages_response = {
133
+ 'data' => [
134
+ { 'name' => 'package-a', 'folder' => true },
135
+ { 'name' => 'package-b', 'folder' => true },
136
+ { 'name' => 'readme.txt', 'folder' => false } # Should be ignored
137
+ ],
138
+ 'continueState' => -1
139
+ }
140
+
141
+ # Mock version listings for each package
142
+ package_a_versions = {
143
+ 'data' => [
144
+ { 'name' => '1.0.0', 'folder' => true },
145
+ { 'name' => '2.0.0', 'folder' => true },
146
+ { 'name' => 'metadata.xml', 'folder' => false } # Should be ignored
147
+ ],
148
+ 'continueState' => -1
149
+ }
150
+
151
+ package_b_versions = {
152
+ 'data' => [
153
+ { 'name' => '0.5.0', 'folder' => true }
154
+ ],
155
+ 'continueState' => -1
156
+ }
157
+
158
+ # Expect calls in order
159
+ expect(mock_client).to receive(:get)
160
+ .with(URI("https://artifacts.example.com/ui/api/v1/ui/v2/nativeBrowser/repo-name/?recordNum=#{Fig::Protocol::Artifactory::INITIAL_LIST_FETCH_SIZE}"))
161
+ .and_return(packages_response)
162
+
163
+ expect(mock_client).to receive(:get)
164
+ .with(URI("https://artifacts.example.com/ui/api/v1/ui/v2/nativeBrowser/repo-name/package-a/?recordNum=#{Fig::Protocol::Artifactory::INITIAL_LIST_FETCH_SIZE}"))
165
+ .and_return(package_a_versions)
166
+
167
+ expect(mock_client).to receive(:get)
168
+ .with(URI("https://artifacts.example.com/ui/api/v1/ui/v2/nativeBrowser/repo-name/package-b/?recordNum=#{Fig::Protocol::Artifactory::INITIAL_LIST_FETCH_SIZE}"))
169
+ .and_return(package_b_versions)
170
+
171
+ result = artifactory.download_list(uri)
172
+ expect(result).to eq([
173
+ 'package-a/1.0.0',
174
+ 'package-a/2.0.0',
175
+ 'package-b/0.5.0'
176
+ ])
177
+ end
178
+ end
179
+
180
+ context 'when repository is empty' do
181
+ it 'returns empty array' do
182
+ empty_response = {
183
+ 'data' => [],
184
+ 'continueState' => -1
185
+ }
186
+
187
+ expect(mock_client).to receive(:get)
188
+ .with(URI("https://artifacts.example.com/ui/api/v1/ui/v2/nativeBrowser/repo-name/?recordNum=#{Fig::Protocol::Artifactory::INITIAL_LIST_FETCH_SIZE}"))
189
+ .and_return(empty_response)
190
+
191
+ result = artifactory.download_list(uri)
192
+ expect(result).to eq([])
193
+ end
194
+ end
195
+
196
+ context 'when API calls fail' do
197
+ it 'handles errors gracefully and continues processing' do
198
+ packages_response = {
199
+ 'data' => [
200
+ { 'name' => 'good-package', 'folder' => true },
201
+ { 'name' => 'bad-package', 'folder' => true }
202
+ ],
203
+ 'continueState' => -1
204
+ }
205
+
206
+ good_versions = {
207
+ 'data' => [{ 'name' => '1.0.0', 'folder' => true }],
208
+ 'continueState' => -1
209
+ }
210
+
211
+ expect(mock_client).to receive(:get)
212
+ .with(URI("https://artifacts.example.com/ui/api/v1/ui/v2/nativeBrowser/repo-name/?recordNum=#{Fig::Protocol::Artifactory::INITIAL_LIST_FETCH_SIZE}"))
213
+ .and_return(packages_response)
214
+
215
+ expect(mock_client).to receive(:get)
216
+ .with(URI("https://artifacts.example.com/ui/api/v1/ui/v2/nativeBrowser/repo-name/good-package/?recordNum=#{Fig::Protocol::Artifactory::INITIAL_LIST_FETCH_SIZE}"))
217
+ .and_return(good_versions)
218
+
219
+ expect(mock_client).to receive(:get)
220
+ .with(URI("https://artifacts.example.com/ui/api/v1/ui/v2/nativeBrowser/repo-name/bad-package/?recordNum=#{Fig::Protocol::Artifactory::INITIAL_LIST_FETCH_SIZE}"))
221
+ .and_raise(StandardError.new('API error'))
222
+
223
+ # Should continue processing despite error
224
+ result = artifactory.download_list(uri)
225
+ expect(result).to eq(['good-package/1.0.0'])
226
+ end
227
+ end
228
+
229
+ context 'with invalid package/version names' do
230
+ it 'filters out names that do not match COMPONENT_PATTERN' do
231
+ packages_response = {
232
+ 'data' => [
233
+ { 'name' => 'valid-package', 'folder' => true },
234
+ { 'name' => 'invalid package with spaces', 'folder' => true },
235
+ { 'name' => 'invalid@symbols', 'folder' => true }
236
+ ],
237
+ 'continueState' => -1
238
+ }
239
+
240
+ valid_versions = {
241
+ 'data' => [
242
+ { 'name' => '1.0.0', 'folder' => true },
243
+ { 'name' => 'invalid version', 'folder' => true }
244
+ ],
245
+ 'continueState' => -1
246
+ }
247
+
248
+ expect(mock_client).to receive(:get)
249
+ .with(URI("https://artifacts.example.com/ui/api/v1/ui/v2/nativeBrowser/repo-name/?recordNum=#{Fig::Protocol::Artifactory::INITIAL_LIST_FETCH_SIZE}"))
250
+ .and_return(packages_response)
251
+
252
+ expect(mock_client).to receive(:get)
253
+ .with(URI("https://artifacts.example.com/ui/api/v1/ui/v2/nativeBrowser/repo-name/valid-package/?recordNum=#{Fig::Protocol::Artifactory::INITIAL_LIST_FETCH_SIZE}"))
254
+ .and_return(valid_versions)
255
+
256
+ result = artifactory.download_list(uri)
257
+ expect(result).to eq(['valid-package/1.0.0'])
258
+ end
259
+ end
260
+ end
261
+
262
+ describe '#download' do
263
+ let(:uri) { URI('artifactory://artifacts.example.com/artifactory/repo-name/package/version/file.tar.gz') }
264
+ let(:https_uri) { URI(uri.to_s.sub(/\Aartifactory:/, 'https:')) }
265
+ let(:path) { '/tmp/test_file.tar.gz' }
266
+ let(:prompt_for_login) { false }
267
+ let(:mock_file) { double('File') }
268
+ let(:mock_auth) { double('Authentication', username: 'testuser', password: 'testpass') }
269
+
270
+ before do
271
+ allow(::File).to receive(:open).with(path, 'wb').and_yield(mock_file)
272
+ allow(mock_file).to receive(:binmode)
273
+ end
274
+
275
+ context 'with authentication' do
276
+ it 'downloads file and logs curl equivalent with auth' do
277
+ allow(artifactory).to receive(:get_authentication_for).with(uri.host, prompt_for_login).and_return(mock_auth)
278
+ allow(artifactory).to receive(:download_via_http_get).with(https_uri.to_s, mock_file)
279
+
280
+ expect(Fig::Logging).to receive(:debug).with("Equivalent curl: curl -u testuser:*** -o '#{path}' '#{https_uri}'")
281
+
282
+ result = artifactory.download(uri, path, prompt_for_login)
283
+ expect(result).to be true
284
+ end
285
+ end
286
+
287
+ context 'without authentication' do
288
+ it 'downloads file and logs curl equivalent without auth' do
289
+ allow(artifactory).to receive(:get_authentication_for).with(uri.host, prompt_for_login).and_return(nil)
290
+ allow(artifactory).to receive(:download_via_http_get).with(https_uri.to_s, mock_file)
291
+
292
+ expect(Fig::Logging).to receive(:debug).with("Equivalent curl: curl -o '#{path}' '#{https_uri}'")
293
+
294
+ result = artifactory.download(uri, path, prompt_for_login)
295
+ expect(result).to be true
296
+ end
297
+ end
298
+
299
+ context 'when download_via_http_get raises SystemCallError' do
300
+ it 'wraps error in FileNotFoundError' do
301
+ allow(artifactory).to receive(:get_authentication_for).and_return(nil)
302
+ system_error = SystemCallError.new('Connection failed')
303
+ allow(artifactory).to receive(:download_via_http_get).and_raise(system_error)
304
+
305
+ expect(Fig::Logging).to receive(:debug).with("Equivalent curl: curl -o '#{path}' '#{https_uri}'")
306
+ expect(Fig::Logging).to receive(:debug).with('unknown error - Connection failed')
307
+ expect { artifactory.download(uri, path, prompt_for_login) }.to raise_error(Fig::FileNotFoundError, 'unknown error - Connection failed')
308
+ end
309
+ end
310
+
311
+ context 'when download_via_http_get raises SocketError' do
312
+ it 'wraps error in FileNotFoundError' do
313
+ allow(artifactory).to receive(:get_authentication_for).and_return(nil)
314
+ socket_error = SocketError.new('Host not found')
315
+ allow(artifactory).to receive(:download_via_http_get).and_raise(socket_error)
316
+
317
+ expect(Fig::Logging).to receive(:debug).with("Equivalent curl: curl -o '#{path}' '#{https_uri}'")
318
+ expect(Fig::Logging).to receive(:debug).with('Host not found')
319
+ expect { artifactory.download(uri, path, prompt_for_login) }.to raise_error(Fig::FileNotFoundError, 'Host not found')
320
+ end
321
+ end
322
+
323
+ it 'opens file in binary write mode' do
324
+ allow(artifactory).to receive(:get_authentication_for).and_return(nil)
325
+ allow(artifactory).to receive(:download_via_http_get)
326
+
327
+ expect(::File).to receive(:open).with(path, 'wb')
328
+ expect(mock_file).to receive(:binmode)
329
+
330
+ artifactory.download(uri, path, prompt_for_login)
331
+ end
332
+ end
333
+
334
+ describe '#httpify_uri' do
335
+ let(:artifactory) { Fig::Protocol::Artifactory.new }
336
+
337
+ it 'converts art:// scheme to https://' do
338
+ art_uri = URI('art://artifacts.example.com/artifactory/repo-name/')
339
+ result = artifactory.send(:httpify_uri, art_uri)
340
+
341
+ expect(result.scheme).to eq('https')
342
+ expect(result.host).to eq('artifacts.example.com')
343
+ expect(result.path).to eq('/artifactory/repo-name/')
344
+ expect(result).to be_a(URI::HTTPS)
345
+ end
346
+
347
+ it 'converts artifactory:// scheme to https://' do
348
+ art_uri = URI('artifactory://artifacts.example.com/artifactory/repo-name/package/version/file.tar.gz')
349
+ result = artifactory.send(:httpify_uri, art_uri)
350
+
351
+ expect(result.scheme).to eq('https')
352
+ expect(result.host).to eq('artifacts.example.com')
353
+ expect(result.path).to eq('/artifactory/repo-name/package/version/file.tar.gz')
354
+ expect(result).to be_a(URI::HTTPS)
355
+ end
356
+
357
+ it 'preserves port and query parameters' do
358
+ art_uri = URI('art://artifacts.example.com:8080/artifactory/repo-name/?param=value')
359
+ result = artifactory.send(:httpify_uri, art_uri)
360
+
361
+ expect(result.scheme).to eq('https')
362
+ expect(result.host).to eq('artifacts.example.com')
363
+ expect(result.port).to eq(8080)
364
+ expect(result.path).to eq('/artifactory/repo-name/')
365
+ expect(result.query).to eq('param=value')
366
+ expect(result).to be_a(URI::HTTPS)
367
+ end
368
+
369
+ it 'handles URIs with userinfo' do
370
+ art_uri = URI('artifactory://user:pass@artifacts.example.com/artifactory/repo-name/')
371
+ result = artifactory.send(:httpify_uri, art_uri)
372
+
373
+ expect(result.scheme).to eq('https')
374
+ expect(result.userinfo).to eq('user:pass')
375
+ expect(result.host).to eq('artifacts.example.com')
376
+ expect(result.path).to eq('/artifactory/repo-name/')
377
+ expect(result).to be_a(URI::HTTPS)
378
+ end
379
+ end
380
+
381
+ describe '#parse_uri' do
382
+ let(:artifactory) { Fig::Protocol::Artifactory.new }
383
+
384
+ context 'with basic artifactory URI' do
385
+ it 'parses repository-level URI correctly' do
386
+ art_uri = URI('art://artifacts.example.com/artifactory/repo-name/')
387
+ result = artifactory.send(:parse_uri, art_uri)
388
+
389
+ expect(result[:repo_key]).to eq('repo-name')
390
+ expect(result[:base_endpoint]).to eq('https://artifacts.example.com/artifactory')
391
+ expect(result[:target_path]).to eq('')
392
+ end
393
+
394
+ it 'parses URI with file path correctly' do
395
+ art_uri = URI('artifactory://artifacts.example.com/artifactory/repo-name/package/version/file.tar.gz')
396
+ result = artifactory.send(:parse_uri, art_uri)
397
+
398
+ expect(result[:repo_key]).to eq('repo-name')
399
+ expect(result[:base_endpoint]).to eq('https://artifacts.example.com/artifactory')
400
+ expect(result[:target_path]).to eq('package/version/file.tar.gz')
401
+ end
402
+
403
+ it 'parses URI with nested directory path correctly' do
404
+ art_uri = URI('art://artifacts.example.com/artifactory/my-repo/com/example/package/1.0.0/package-1.0.0.jar')
405
+ result = artifactory.send(:parse_uri, art_uri)
406
+
407
+ expect(result[:repo_key]).to eq('my-repo')
408
+ expect(result[:base_endpoint]).to eq('https://artifacts.example.com/artifactory')
409
+ expect(result[:target_path]).to eq('com/example/package/1.0.0/package-1.0.0.jar')
410
+ end
411
+ end
412
+
413
+ context 'with custom port' do
414
+ it 'includes port in base_endpoint when non-standard' do
415
+ art_uri = URI('artifactory://artifacts.example.com:8080/artifactory/repo-name/file.txt')
416
+ result = artifactory.send(:parse_uri, art_uri)
417
+
418
+ expect(result[:repo_key]).to eq('repo-name')
419
+ expect(result[:base_endpoint]).to eq('https://artifacts.example.com:8080/artifactory')
420
+ expect(result[:target_path]).to eq('file.txt')
421
+ end
422
+
423
+ it 'excludes default HTTPS port from base_endpoint' do
424
+ art_uri = URI('art://artifacts.example.com:443/artifactory/repo-name/file.txt')
425
+ result = artifactory.send(:parse_uri, art_uri)
426
+
427
+ expect(result[:repo_key]).to eq('repo-name')
428
+ expect(result[:base_endpoint]).to eq('https://artifacts.example.com/artifactory')
429
+ expect(result[:target_path]).to eq('file.txt')
430
+ end
431
+ end
432
+
433
+ context 'with artifactory in different path positions' do
434
+ it 'handles artifactory in root path' do
435
+ art_uri = URI('art://artifacts.example.com/artifactory/repo-name/file.txt')
436
+ result = artifactory.send(:parse_uri, art_uri)
437
+
438
+ expect(result[:repo_key]).to eq('repo-name')
439
+ expect(result[:base_endpoint]).to eq('https://artifacts.example.com/artifactory')
440
+ expect(result[:target_path]).to eq('file.txt')
441
+ end
442
+
443
+ it 'handles artifactory in nested path' do
444
+ art_uri = URI('artifactory://artifacts.example.com/some/path/artifactory/repo-name/file.txt')
445
+ result = artifactory.send(:parse_uri, art_uri)
446
+
447
+ expect(result[:repo_key]).to eq('repo-name')
448
+ expect(result[:base_endpoint]).to eq('https://artifacts.example.com/some/path/artifactory')
449
+ expect(result[:target_path]).to eq('file.txt')
450
+ end
451
+ end
452
+
453
+ context 'with trailing slashes' do
454
+ it 'handles URI with trailing slash' do
455
+ art_uri = URI('art://artifacts.example.com/artifactory/repo-name/')
456
+ result = artifactory.send(:parse_uri, art_uri)
457
+
458
+ expect(result[:repo_key]).to eq('repo-name')
459
+ expect(result[:base_endpoint]).to eq('https://artifacts.example.com/artifactory')
460
+ expect(result[:target_path]).to eq('')
461
+ end
462
+
463
+ it 'handles URI without trailing slash' do
464
+ art_uri = URI('artifactory://artifacts.example.com/artifactory/repo-name')
465
+ result = artifactory.send(:parse_uri, art_uri)
466
+
467
+ expect(result[:repo_key]).to eq('repo-name')
468
+ expect(result[:base_endpoint]).to eq('https://artifacts.example.com/artifactory')
469
+ expect(result[:target_path]).to eq('')
470
+ end
471
+ end
472
+
473
+ context 'error cases' do
474
+ it 'raises ArgumentError when artifactory is not in path' do
475
+ art_uri = URI('art://artifacts.example.com/some/other/path/repo-name/')
476
+
477
+ expect {
478
+ artifactory.send(:parse_uri, art_uri)
479
+ }.to raise_error(ArgumentError, /URI must contain 'artifactory' in path/)
480
+ end
481
+
482
+ it 'raises ArgumentError when no repository key is found' do
483
+ art_uri = URI('artifactory://artifacts.example.com/artifactory/')
484
+
485
+ expect {
486
+ artifactory.send(:parse_uri, art_uri)
487
+ }.to raise_error(ArgumentError, /No repository key found in URI/)
488
+ end
489
+
490
+ it 'raises ArgumentError when artifactory is at end of path' do
491
+ art_uri = URI('art://artifacts.example.com/some/path/artifactory')
492
+
493
+ expect {
494
+ artifactory.send(:parse_uri, art_uri)
495
+ }.to raise_error(ArgumentError, /No repository key found in URI/)
496
+ end
497
+ end
498
+
499
+ context 'edge cases' do
500
+ it 'handles empty target path correctly' do
501
+ art_uri = URI('artifactory://artifacts.example.com/artifactory/repo-name')
502
+ result = artifactory.send(:parse_uri, art_uri)
503
+
504
+ expect(result[:target_path]).to eq('')
505
+ end
506
+
507
+ it 'handles single character repo key' do
508
+ art_uri = URI('art://artifacts.example.com/artifactory/r/file.txt')
509
+ result = artifactory.send(:parse_uri, art_uri)
510
+
511
+ expect(result[:repo_key]).to eq('r')
512
+ expect(result[:target_path]).to eq('file.txt')
513
+ end
514
+
515
+ it 'handles repo key with special characters' do
516
+ art_uri = URI('artifactory://artifacts.example.com/artifactory/repo-name-with-dashes_and_underscores/file.txt')
517
+ result = artifactory.send(:parse_uri, art_uri)
518
+
519
+ expect(result[:repo_key]).to eq('repo-name-with-dashes_and_underscores')
520
+ expect(result[:target_path]).to eq('file.txt')
521
+ end
522
+ end
523
+ end
524
+
525
+ describe '#upload' do
526
+ let(:local_file) { '/tmp/test.txt' }
527
+ let(:uri) { URI('https://artifacts.example.com/artifactory/repo-name/path/to/file.txt') }
528
+ let(:mock_client) { double('Artifactory::Client') }
529
+ let(:mock_artifact) { double('Artifactory::Resource::Artifact') }
530
+ let(:mock_authentication) { double('authentication', username: 'testuser', password: 'testpass') }
531
+
532
+ before do
533
+ allow(artifactory).to receive(:get_authentication_for).and_return(mock_authentication)
534
+ # Mock the global configuration - create a config double that responds to all setters
535
+ config_mock = double('config')
536
+ allow(config_mock).to receive(:endpoint=)
537
+ allow(config_mock).to receive(:username=)
538
+ allow(config_mock).to receive(:password=)
539
+ allow(::Artifactory).to receive(:configure).and_yield(config_mock)
540
+ allow(::Artifactory::Resource::Artifact).to receive(:new).and_return(mock_artifact)
541
+ allow(::File).to receive(:stat).and_return(double('stat', size: 1024, mtime: Time.now))
542
+ allow(Digest::SHA1).to receive(:file).and_return(double(hexdigest: 'sha1hash'))
543
+ allow(Digest::MD5).to receive(:file).and_return(double(hexdigest: 'md5hash'))
544
+ end
545
+
546
+ it 'parses URI correctly and uploads file' do
547
+ config_mock = double('config')
548
+ expect(::Artifactory).to receive(:configure).and_yield(config_mock)
549
+ expect(config_mock).to receive(:endpoint=).with('https://artifacts.example.com/artifactory')
550
+ expect(config_mock).to receive(:username=).with('testuser')
551
+ expect(config_mock).to receive(:password=).with('testpass')
552
+
553
+ expect(mock_artifact).to receive(:upload).with(
554
+ 'repo-name',
555
+ 'path/to/file.txt',
556
+ hash_including('fig.original_path' => local_file)
557
+ )
558
+
559
+ artifactory.upload(local_file, uri)
560
+ end
561
+
562
+ it 'logs equivalent curl command' do
563
+ allow(mock_artifact).to receive(:upload)
564
+
565
+ expect(Fig::Logging).to receive(:debug).with(
566
+ "Equivalent curl: curl -u testuser:*** -T '#{local_file}' '#{uri}'"
567
+ ).ordered
568
+ expect(Fig::Logging).to receive(:debug).with(
569
+ /Upload metadata:/
570
+ ).ordered
571
+
572
+ artifactory.upload(local_file, uri)
573
+ end
574
+
575
+ it 'raises error for invalid URI without artifactory in path' do
576
+ invalid_uri = URI('https://example.com/repo-name/file.txt')
577
+
578
+ expect {
579
+ artifactory.upload(local_file, invalid_uri)
580
+ }.to raise_error(ArgumentError, /URI must contain 'artifactory' in path/)
581
+ end
582
+
583
+ it 'collects metadata with fig. prefix' do
584
+ metadata_hash = nil
585
+ allow(mock_artifact).to receive(:upload) do |repo, path, metadata|
586
+ metadata_hash = metadata
587
+ end
588
+
589
+ artifactory.upload(local_file, uri)
590
+
591
+ expect(metadata_hash).to include(
592
+ 'fig.original_path' => local_file,
593
+ 'fig.target_path' => 'path/to/file.txt',
594
+ 'fig.tool' => 'fig-artifactory-protocol'
595
+ )
596
+ expect(metadata_hash.keys).to all(start_with('fig.'))
597
+ end
598
+ end
599
+ end