fig 2.0.0 → 2.1.0.pre.alpha.1

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f11dd490ac715ced00c3f38d9ebed58d5425d267c82663b8cd2597063b0d98c7
4
- data.tar.gz: 04162515e74a82aa25407804f46651f5dc0bdbff85d2a101a12bcc5b1eb49d6a
3
+ metadata.gz: a99ef226fefef4e3851e250d0ce01dacb7c4fc3bf9408ec697ea11a88b3ba471
4
+ data.tar.gz: f1ce4ec0076a2b2e5de61e4fbe52fb93de1c00ff9cb4427e3fb21e14a7f39a8e
5
5
  SHA512:
6
- metadata.gz: 459277b3fae615ee4257ce7ee9fb91c2c5f4097a5117a4a4bee2b1f7ca3179897cbdd40721dd31bac41b1be3c65b346366bbf0218bb6714f912e359101d04125
7
- data.tar.gz: 96aa5db0a926f77d875d83e6ec4749e64a2331ed7ef3fa761045c1a914e72b91c400955e6420b12d0c252223e8b419e7d95477b2e714a3fd50d93d3425a3ea92
6
+ metadata.gz: d680a96c48d21158c472fc40a9f1141a6aac72353d1c456e2a5a8a763c12b05fe63c6e891699aba27525492a37ab8c6ce76271f8f2156cbdf85f3c72eddbcf19
7
+ data.tar.gz: c11a5555b761e03be9b6367f98359cbbb1f1839b2d83237aff55222f984f6dc8b1483b18be9978cdcf7f99b9cf5276316d9fc9b852c2ffb72c6b79d61829105f
@@ -88,7 +88,7 @@ class Fig::OperatingSystem
88
88
  @protocols['https'] = Fig::Protocol::HTTP.new
89
89
  @protocols['sftp'] = Fig::Protocol::SFTP.new
90
90
  @protocols['ssh'] = Fig::Protocol::SSH.new
91
- @protocols['art'] = @protocols['artifactory'] = Fig::Protocol::Artifactory.new
91
+ @protocols['art'] = @protocols['artifactory'] = Fig::Protocol::Artifactory.new login
92
92
  end
93
93
 
94
94
  def list(dir)
@@ -32,10 +32,22 @@ class Fig::Protocol::Artifactory
32
32
  # Default number of list entries to fetch on initial iteration
33
33
  INITIAL_LIST_FETCH_SIZE = 20000
34
34
 
35
- def initialize
35
+ def initialize(login)
36
+ @login = login
36
37
  initialize_netrc
37
38
  end
38
39
 
40
+ # like ftp's ftp_login
41
+ def artifactory_auth(client_config, host, prompt_for_login)
42
+ if @login
43
+ auth = get_authentication_for(host, prompt_for_login)
44
+ if auth
45
+ client_config[:username] = auth.username
46
+ client_config[:password] = auth.password
47
+ end
48
+ end
49
+ end
50
+
39
51
  # must return a list of strings in the form <package_name>/<version>
40
52
  def download_list(uri)
41
53
  Fig::Logging.info("Downloading list of packages at #{uri}")
@@ -47,12 +59,8 @@ class Fig::Protocol::Artifactory
47
59
  parse_uri(uri) => { repo_key:, base_endpoint: }
48
60
 
49
61
  # Create Artifactory client instance
50
- authentication = get_authentication_for(uri.host, :prompt_for_login)
51
62
  client_config = { endpoint: base_endpoint }
52
- if authentication
53
- client_config[:username] = authentication.username
54
- client_config[:password] = authentication.password
55
- end
63
+ artifactory_auth(client_config, uri.host, :prompt_for_login)
56
64
  client = ::Artifactory::Client.new(client_config)
57
65
 
58
66
  # Use Artifactory browser API to list directories at repo root
@@ -85,9 +93,11 @@ class Fig::Protocol::Artifactory
85
93
  uri = httpify_uri(art_uri)
86
94
 
87
95
  # Log equivalent curl command for debugging
88
- authentication = get_authentication_for(uri.host, prompt_for_login)
89
- if authentication
90
- Fig::Logging.debug("Equivalent curl: curl -u #{authentication.username}:*** -o '#{path}' '#{uri}'")
96
+ client_config = { endpoint: art_uri }
97
+ artifactory_auth(client_config, uri.host, prompt_for_login)
98
+
99
+ if client_config[:username]
100
+ Fig::Logging.debug("Equivalent curl: curl -u #{client_config[:username]}:*** -o '#{path}' '#{uri}'")
91
101
  else
92
102
  Fig::Logging.debug("Equivalent curl: curl -o '#{path}' '#{uri}'")
93
103
  end
@@ -96,7 +106,7 @@ class Fig::Protocol::Artifactory
96
106
  file.binmode
97
107
 
98
108
  begin
99
- download_via_http_get(uri.to_s, file)
109
+ download_via_http_get(uri.to_s, file, client_config)
100
110
  rescue SystemCallError => error
101
111
  Fig::Logging.debug error.message
102
112
  raise Fig::FileNotFoundError.new error.message, uri
@@ -115,21 +125,17 @@ class Fig::Protocol::Artifactory
115
125
  begin
116
126
  parse_uri(uri) => { repo_key:, base_endpoint:, target_path: }
117
127
 
128
+ client_config = { endpoint: base_endpoint }
129
+ artifactory_auth(client_config, uri.host, :prompt_for_login)
130
+
118
131
  # Configure Artifactory gem globally - unlike other methods that can use client instances,
119
132
  # the artifact.upload() method ignores the client: parameter and only uses global config.
120
133
  # This is a limitation of the artifactory gem's upload implementation.
121
- authentication = get_authentication_for(uri.host, :prompt_for_login)
122
- ::Artifactory.configure do |config|
123
- config.endpoint = base_endpoint
124
- if authentication
125
- config.username = authentication.username
126
- config.password = authentication.password
127
- end
128
- end
134
+ ::Artifactory.configure{ |c| client_config.each{|k,v| c.public_send("#{k}=",v)} } # thx o4!
129
135
 
130
136
  # Log equivalent curl command for debugging
131
- if authentication
132
- Fig::Logging.debug("Equivalent curl: curl -u #{authentication.username}:*** -T '#{local_file}' '#{uri}'")
137
+ if client_config[:username]
138
+ Fig::Logging.debug("Equivalent curl: curl -u #{client_config[:username]}:*** -T '#{local_file}' '#{uri}'")
133
139
  else
134
140
  Fig::Logging.debug("Equivalent curl: curl -T '#{local_file}' '#{uri}'")
135
141
  end
@@ -163,12 +169,8 @@ class Fig::Protocol::Artifactory
163
169
 
164
170
 
165
171
  # Create Artifactory client instance (same as upload method)
166
- authentication = get_authentication_for(uri.host, prompt_for_login)
167
172
  client_config = { endpoint: base_endpoint }
168
- if authentication
169
- client_config[:username] = authentication.username
170
- client_config[:password] = authentication.password
171
- end
173
+ artifactory_auth(client_config, uri.host, :prompt_for_login)
172
174
  client = ::Artifactory::Client.new(client_config)
173
175
 
174
176
  # use storage api instead of search - more reliable for virtual repos
@@ -363,14 +365,26 @@ class Fig::Protocol::Artifactory
363
365
  end
364
366
  end
365
367
 
366
- # swiped directly from http.rb; if no changes are required, then consider refactoring
367
- def download_via_http_get(uri_string, file, redirection_limit = 10)
368
+ # HTTP download with optional authentication support
369
+ # If auth_config contains :username and :password, uses HTTP basic auth
370
+ def download_via_http_get(uri_string, file, auth_config = {}, redirection_limit = 10)
368
371
  if redirection_limit < 1
369
372
  Fig::Logging.debug 'Too many HTTP redirects.'
370
373
  raise Fig::FileNotFoundError.new 'Too many HTTP redirects.', uri_string
371
374
  end
372
375
 
373
- response = Net::HTTP.get_response(URI(uri_string))
376
+ uri = URI(uri_string)
377
+
378
+ response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
379
+ request = Net::HTTP::Get.new(uri.request_uri)
380
+
381
+ # Add HTTP basic auth if credentials provided
382
+ if auth_config[:username] && auth_config[:password]
383
+ request.basic_auth(auth_config[:username], auth_config[:password])
384
+ end
385
+
386
+ http.request(request)
387
+ end
374
388
 
375
389
  case response
376
390
  when Net::HTTPSuccess then
@@ -378,7 +392,7 @@ class Fig::Protocol::Artifactory
378
392
  when Net::HTTPRedirection then
379
393
  location = response['location']
380
394
  Fig::Logging.debug "Redirecting to #{location}."
381
- download_via_http_get(location, file, redirection_limit - 1)
395
+ download_via_http_get(location, file, auth_config, redirection_limit - 1)
382
396
  else
383
397
  Fig::Logging.debug "Download failed: #{response.code} #{response.message}."
384
398
  raise Fig::FileNotFoundError.new(
data/lib/fig/version.rb CHANGED
@@ -1,5 +1,8 @@
1
1
  # coding: utf-8
2
2
 
3
+ # version format:
4
+ # pre-release: M.m.p-<pretag>.X eg, 2.0.0-alpha.23
5
+ # release: M.m.p eg, 2.0.0
3
6
  module Fig
4
- VERSION = '2.0.0'.freeze
7
+ VERSION = '2.1.0-alpha.1'.freeze
5
8
  end
@@ -4,7 +4,7 @@ require 'spec_helper'
4
4
  require 'fig/protocol/artifactory'
5
5
 
6
6
  describe Fig::Protocol::Artifactory do
7
- let(:artifactory) { Fig::Protocol::Artifactory.new }
7
+ let(:artifactory) { Fig::Protocol::Artifactory.new nil }
8
8
  let(:base_url) { URI('https://artifacts.example.com/artifactory/ui/api/v1/ui/v2/nativeBrowser/repo-name/') }
9
9
 
10
10
  describe '#get_all_artifactory_entries' do
@@ -117,17 +117,20 @@ describe Fig::Protocol::Artifactory do
117
117
 
118
118
  describe '#download_list' do
119
119
  let(:uri) { URI('art://artifacts.example.com/artifactory/repo-name/') }
120
- let(:artifactory) { Fig::Protocol::Artifactory.new }
121
120
  let(:mock_client) { double('Artifactory::Client') }
122
121
 
123
122
  before do
124
123
  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
124
  end
128
125
 
129
- context 'when repository has packages and versions' do
130
- it 'returns sorted package/version strings' do
126
+ context 'basic functionality' do
127
+ let(:artifactory) { Fig::Protocol::Artifactory.new nil }
128
+
129
+ before do
130
+ allow(artifactory).to receive(:get_authentication_for).and_return(nil)
131
+ end
132
+
133
+ it 'returns sorted package/version strings when repository has packages and versions' do
131
134
  # Mock package listing
132
135
  packages_response = {
133
136
  'data' => [
@@ -175,10 +178,8 @@ describe Fig::Protocol::Artifactory do
175
178
  'package-b/0.5.0'
176
179
  ])
177
180
  end
178
- end
179
181
 
180
- context 'when repository is empty' do
181
- it 'returns empty array' do
182
+ it 'returns empty array when repository is empty' do
182
183
  empty_response = {
183
184
  'data' => [],
184
185
  'continueState' => -1
@@ -191,10 +192,8 @@ describe Fig::Protocol::Artifactory do
191
192
  result = artifactory.download_list(uri)
192
193
  expect(result).to eq([])
193
194
  end
194
- end
195
195
 
196
- context 'when API calls fail' do
197
- it 'handles errors gracefully and continues processing' do
196
+ it 'handles API call errors gracefully and continues processing' do
198
197
  packages_response = {
199
198
  'data' => [
200
199
  { 'name' => 'good-package', 'folder' => true },
@@ -224,10 +223,8 @@ describe Fig::Protocol::Artifactory do
224
223
  result = artifactory.download_list(uri)
225
224
  expect(result).to eq(['good-package/1.0.0'])
226
225
  end
227
- end
228
226
 
229
- context 'with invalid package/version names' do
230
- it 'filters out names that do not match COMPONENT_PATTERN' do
227
+ it 'filters out invalid package/version names that do not match COMPONENT_PATTERN' do
231
228
  packages_response = {
232
229
  'data' => [
233
230
  { 'name' => 'valid-package', 'folder' => true },
@@ -257,6 +254,123 @@ describe Fig::Protocol::Artifactory do
257
254
  expect(result).to eq(['valid-package/1.0.0'])
258
255
  end
259
256
  end
257
+
258
+ context 'authentication scenarios' do
259
+ let(:mock_auth) { double('authentication', username: 'testuser', password: 'testpass') }
260
+ let(:packages_response) { { 'data' => [{ 'name' => 'pkg', 'folder' => true }], 'continueState' => -1 } }
261
+ let(:versions_response) { { 'data' => [{ 'name' => '1.0', 'folder' => true }], 'continueState' => -1 } }
262
+
263
+ before do
264
+ allow(mock_client).to receive(:get)
265
+ .with(URI("https://artifacts.example.com/ui/api/v1/ui/v2/nativeBrowser/repo-name/?recordNum=#{Fig::Protocol::Artifactory::INITIAL_LIST_FETCH_SIZE}"))
266
+ .and_return(packages_response)
267
+ allow(mock_client).to receive(:get)
268
+ .with(URI("https://artifacts.example.com/ui/api/v1/ui/v2/nativeBrowser/repo-name/pkg/?recordNum=#{Fig::Protocol::Artifactory::INITIAL_LIST_FETCH_SIZE}"))
269
+ .and_return(versions_response)
270
+ end
271
+
272
+ [
273
+ { login: nil, auth: nil, desc: 'without login' },
274
+ { login: true, auth: :mock_auth, desc: 'with authentication' },
275
+ { login: true, auth: nil, desc: 'with login but no authentication found' }
276
+ ].each do |scenario|
277
+ context "#{scenario[:desc]}: login=#{scenario[:login]}, auth=#{scenario[:auth] || 'none'}" do
278
+ let(:artifactory) { Fig::Protocol::Artifactory.new scenario[:login] }
279
+ let(:auth) { scenario[:auth] == :mock_auth ? mock_auth : nil }
280
+
281
+ it 'successfully lists packages regardless of auth state' do
282
+ allow(artifactory).to receive(:get_authentication_for).and_return(auth)
283
+
284
+ result = artifactory.download_list(uri)
285
+ expect(result).to eq(['pkg/1.0'])
286
+ end
287
+ end
288
+ end
289
+ end
290
+ end
291
+
292
+ describe '#path_up_to_date?' do
293
+ let(:uri) { URI('art://artifacts.example.com/artifactory/repo-name/package/version/file.tar.gz') }
294
+ let(:local_path) { '/tmp/local_file.tar.gz' }
295
+ let(:mock_client) { double('Artifactory::Client') }
296
+ let(:mock_auth) { double('authentication', username: 'testuser', password: 'testpass') }
297
+ let(:remote_mtime) { Time.parse('2024-01-15 10:00:00 UTC') }
298
+ let(:local_mtime) { Time.parse('2024-01-15 09:00:00 UTC') }
299
+ let(:remote_size) { 1024 }
300
+ let(:local_size) { 1024 }
301
+
302
+ before do
303
+ allow(::Artifactory::Client).to receive(:new).and_return(mock_client)
304
+ allow(::File).to receive(:size).with(local_path).and_return(local_size)
305
+ allow(::File).to receive(:mtime).with(local_path).and_return(local_mtime)
306
+ end
307
+
308
+ context 'file up-to-date checks' do
309
+ let(:artifactory) { Fig::Protocol::Artifactory.new nil }
310
+ let(:storage_response) { { 'size' => remote_size, 'lastModified' => remote_mtime.iso8601 } }
311
+
312
+ before do
313
+ allow(artifactory).to receive(:get_authentication_for).and_return(nil)
314
+ end
315
+
316
+ it 'returns true when remote file is older or same age and same size' do
317
+ allow(mock_client).to receive(:get).with('/api/storage/repo-name/package/version/file.tar.gz').and_return(storage_response)
318
+
319
+ result = artifactory.path_up_to_date?(uri, local_path, false)
320
+ expect(result).to be false # remote is newer
321
+ end
322
+
323
+ it 'returns true when local file is newer or same age and same size' do
324
+ newer_local = Time.parse('2024-01-15 11:00:00 UTC')
325
+ allow(::File).to receive(:mtime).with(local_path).and_return(newer_local)
326
+ allow(mock_client).to receive(:get).with('/api/storage/repo-name/package/version/file.tar.gz').and_return(storage_response)
327
+
328
+ result = artifactory.path_up_to_date?(uri, local_path, false)
329
+ expect(result).to be true
330
+ end
331
+
332
+ it 'returns false when sizes differ' do
333
+ allow(::File).to receive(:size).with(local_path).and_return(2048)
334
+ allow(mock_client).to receive(:get).with('/api/storage/repo-name/package/version/file.tar.gz').and_return(storage_response)
335
+
336
+ result = artifactory.path_up_to_date?(uri, local_path, false)
337
+ expect(result).to be false
338
+ end
339
+
340
+ it 'returns nil when API call fails' do
341
+ allow(mock_client).to receive(:get).and_raise(StandardError.new('API error'))
342
+
343
+ result = artifactory.path_up_to_date?(uri, local_path, false)
344
+ expect(result).to be_nil
345
+ end
346
+ end
347
+
348
+ context 'authentication scenarios' do
349
+ let(:storage_response) { { 'size' => remote_size, 'lastModified' => remote_mtime.iso8601 } }
350
+
351
+ before do
352
+ allow(::File).to receive(:mtime).with(local_path).and_return(Time.parse('2024-01-15 11:00:00 UTC'))
353
+ allow(mock_client).to receive(:get).with('/api/storage/repo-name/package/version/file.tar.gz').and_return(storage_response)
354
+ end
355
+
356
+ [
357
+ { login: nil, auth: nil, desc: 'without login' },
358
+ { login: true, auth: :mock_auth, desc: 'with authentication' },
359
+ { login: true, auth: nil, desc: 'with login but no authentication found' }
360
+ ].each do |scenario|
361
+ context "#{scenario[:desc]}: login=#{scenario[:login]}, auth=#{scenario[:auth] || 'none'}" do
362
+ let(:artifactory) { Fig::Protocol::Artifactory.new scenario[:login] }
363
+ let(:auth) { scenario[:auth] == :mock_auth ? mock_auth : nil }
364
+
365
+ it 'checks file status regardless of auth state' do
366
+ allow(artifactory).to receive(:get_authentication_for).and_return(auth)
367
+
368
+ result = artifactory.path_up_to_date?(uri, local_path, false)
369
+ expect(result).to be true
370
+ end
371
+ end
372
+ end
373
+ end
260
374
  end
261
375
 
262
376
  describe '#download' do
@@ -272,32 +386,31 @@ describe Fig::Protocol::Artifactory do
272
386
  allow(mock_file).to receive(:binmode)
273
387
  end
274
388
 
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
389
+ [
390
+ { login: nil, auth: nil, curl: "curl -o", desc: 'without login' },
391
+ { login: true, auth: :mock_auth, curl: "curl -u testuser:***", desc: 'with authentication' },
392
+ { login: true, auth: nil, curl: "curl -o", desc: 'with login but no authentication found' }
393
+ ].each do |scenario|
394
+ context "#{scenario[:desc]}: login=#{scenario[:login]}, auth=#{scenario[:auth] || 'none'}" do
395
+ let(:artifactory) { Fig::Protocol::Artifactory.new scenario[:login] }
396
+ let(:auth) { scenario[:auth] == :mock_auth ? mock_auth : nil }
286
397
 
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)
398
+ it 'downloads file and logs curl equivalent' do
399
+ allow(artifactory).to receive(:get_authentication_for).with(uri.host, prompt_for_login).and_return(auth)
400
+ allow(artifactory).to receive(:download_via_http_get)
291
401
 
292
- expect(Fig::Logging).to receive(:debug).with("Equivalent curl: curl -o '#{path}' '#{https_uri}'")
402
+ expect(Fig::Logging).to receive(:debug).with(/#{Regexp.escape(scenario[:curl])}.*#{Regexp.escape(path)}.*#{Regexp.escape(https_uri.to_s)}/)
293
403
 
294
- result = artifactory.download(uri, path, prompt_for_login)
295
- expect(result).to be true
404
+ result = artifactory.download(uri, path, prompt_for_login)
405
+ expect(result).to be true
406
+ end
296
407
  end
297
408
  end
298
409
 
299
- context 'when download_via_http_get raises SystemCallError' do
300
- it 'wraps error in FileNotFoundError' do
410
+ context 'error handling' do
411
+ let(:artifactory) { Fig::Protocol::Artifactory.new nil }
412
+
413
+ it 'wraps SystemCallError in FileNotFoundError' do
301
414
  allow(artifactory).to receive(:get_authentication_for).and_return(nil)
302
415
  system_error = SystemCallError.new('Connection failed')
303
416
  allow(artifactory).to receive(:download_via_http_get).and_raise(system_error)
@@ -306,10 +419,8 @@ describe Fig::Protocol::Artifactory do
306
419
  expect(Fig::Logging).to receive(:debug).with('unknown error - Connection failed')
307
420
  expect { artifactory.download(uri, path, prompt_for_login) }.to raise_error(Fig::FileNotFoundError, 'unknown error - Connection failed')
308
421
  end
309
- end
310
422
 
311
- context 'when download_via_http_get raises SocketError' do
312
- it 'wraps error in FileNotFoundError' do
423
+ it 'wraps SocketError in FileNotFoundError' do
313
424
  allow(artifactory).to receive(:get_authentication_for).and_return(nil)
314
425
  socket_error = SocketError.new('Host not found')
315
426
  allow(artifactory).to receive(:download_via_http_get).and_raise(socket_error)
@@ -320,19 +431,51 @@ describe Fig::Protocol::Artifactory do
320
431
  end
321
432
  end
322
433
 
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)
434
+ context 'file handling' do
435
+ let(:artifactory) { Fig::Protocol::Artifactory.new nil }
436
+
437
+ it 'opens file in binary write mode' do
438
+ allow(artifactory).to receive(:get_authentication_for).and_return(nil)
439
+ allow(artifactory).to receive(:download_via_http_get)
326
440
 
327
- expect(::File).to receive(:open).with(path, 'wb')
328
- expect(mock_file).to receive(:binmode)
441
+ expect(::File).to receive(:open).with(path, 'wb')
442
+ expect(mock_file).to receive(:binmode)
329
443
 
330
- artifactory.download(uri, path, prompt_for_login)
444
+ artifactory.download(uri, path, prompt_for_login)
445
+ end
446
+ end
447
+
448
+ context 'HTTP authentication' do
449
+ let(:artifactory) { Fig::Protocol::Artifactory.new true }
450
+
451
+ it 'passes credentials to download_via_http_get when authentication available' do
452
+ allow(artifactory).to receive(:get_authentication_for).and_return(mock_auth)
453
+
454
+ # Verify download_via_http_get is called with auth config containing credentials
455
+ expect(artifactory).to receive(:download_via_http_get) do |uri_str, file, auth_config|
456
+ expect(auth_config[:username]).to eq('testuser')
457
+ expect(auth_config[:password]).to eq('testpass')
458
+ end
459
+
460
+ artifactory.download(uri, path, prompt_for_login)
461
+ end
462
+
463
+ it 'passes empty auth config to download_via_http_get when no credentials' do
464
+ allow(artifactory).to receive(:get_authentication_for).and_return(nil)
465
+
466
+ # Verify download_via_http_get is called without credentials
467
+ expect(artifactory).to receive(:download_via_http_get) do |uri_str, file, auth_config|
468
+ expect(auth_config[:username]).to be_nil
469
+ expect(auth_config[:password]).to be_nil
470
+ end
471
+
472
+ artifactory.download(uri, path, prompt_for_login)
473
+ end
331
474
  end
332
475
  end
333
476
 
334
477
  describe '#httpify_uri' do
335
- let(:artifactory) { Fig::Protocol::Artifactory.new }
478
+ let(:artifactory) { Fig::Protocol::Artifactory.new nil }
336
479
 
337
480
  it 'converts art:// scheme to https://' do
338
481
  art_uri = URI('art://artifacts.example.com/artifactory/repo-name/')
@@ -379,7 +522,7 @@ describe Fig::Protocol::Artifactory do
379
522
  end
380
523
 
381
524
  describe '#parse_uri' do
382
- let(:artifactory) { Fig::Protocol::Artifactory.new }
525
+ let(:artifactory) { Fig::Protocol::Artifactory.new nil }
383
526
 
384
527
  context 'with basic artifactory URI' do
385
528
  it 'parses repository-level URI correctly' do
@@ -525,75 +668,102 @@ describe Fig::Protocol::Artifactory do
525
668
  describe '#upload' do
526
669
  let(:local_file) { '/tmp/test.txt' }
527
670
  let(:uri) { URI('https://artifacts.example.com/artifactory/repo-name/path/to/file.txt') }
528
- let(:mock_client) { double('Artifactory::Client') }
529
671
  let(:mock_artifact) { double('Artifactory::Resource::Artifact') }
530
- let(:mock_authentication) { double('authentication', username: 'testuser', password: 'testpass') }
672
+ let(:mock_auth) { double('authentication', username: 'testuser', password: 'testpass') }
531
673
 
532
674
  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
675
  allow(::Artifactory::Resource::Artifact).to receive(:new).and_return(mock_artifact)
541
676
  allow(::File).to receive(:stat).and_return(double('stat', size: 1024, mtime: Time.now))
542
677
  allow(Digest::SHA1).to receive(:file).and_return(double(hexdigest: 'sha1hash'))
543
678
  allow(Digest::MD5).to receive(:file).and_return(double(hexdigest: 'md5hash'))
544
679
  end
545
680
 
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
- )
681
+ [
682
+ { login: nil, auth: nil, expects_creds: false, curl: "curl -T", desc: 'without login' },
683
+ { login: true, auth: :mock_auth, expects_creds: true, curl: "curl -u testuser:*** -T", desc: 'with authentication' },
684
+ { login: true, auth: nil, expects_creds: false, curl: "curl -T", desc: 'with login but no authentication found' }
685
+ ].each do |scenario|
686
+ context "#{scenario[:desc]}: login=#{scenario[:login]}, auth=#{scenario[:auth] || 'none'}" do
687
+ let(:artifactory) { Fig::Protocol::Artifactory.new scenario[:login] }
688
+ let(:auth) { scenario[:auth] == :mock_auth ? mock_auth : nil }
558
689
 
559
- artifactory.upload(local_file, uri)
560
- end
690
+ before do
691
+ allow(artifactory).to receive(:get_authentication_for).and_return(auth)
692
+ end
561
693
 
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
694
+ it 'parses URI correctly and uploads file' do
695
+ config_mock = double('config')
696
+ expect(::Artifactory).to receive(:configure).and_yield(config_mock)
697
+ expect(config_mock).to receive(:endpoint=).with('https://artifacts.example.com/artifactory')
698
+
699
+ if scenario[:expects_creds]
700
+ expect(config_mock).to receive(:username=).with('testuser')
701
+ expect(config_mock).to receive(:password=).with('testpass')
702
+ else
703
+ expect(config_mock).not_to receive(:username=)
704
+ expect(config_mock).not_to receive(:password=)
705
+ end
706
+
707
+ expect(mock_artifact).to receive(:upload).with(
708
+ 'repo-name',
709
+ 'path/to/file.txt',
710
+ hash_including('fig.original_path' => local_file)
711
+ )
712
+
713
+ artifactory.upload(local_file, uri)
714
+ end
715
+
716
+ it 'logs equivalent curl command' do
717
+ config_mock = double('config')
718
+ allow(config_mock).to receive(:endpoint=)
719
+ allow(config_mock).to receive(:username=)
720
+ allow(config_mock).to receive(:password=)
721
+ allow(::Artifactory).to receive(:configure).and_yield(config_mock)
722
+ allow(mock_artifact).to receive(:upload)
723
+
724
+ expect(Fig::Logging).to receive(:debug).with(/#{Regexp.escape(scenario[:curl])}/).ordered
725
+ expect(Fig::Logging).to receive(:debug).with(/Upload metadata:/).ordered
574
726
 
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/)
727
+ artifactory.upload(local_file, uri)
728
+ end
729
+ end
581
730
  end
582
731
 
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
732
+ context 'error handling' do
733
+ let(:artifactory) { Fig::Protocol::Artifactory.new nil }
734
+
735
+ it 'raises error for invalid URI without artifactory in path' do
736
+ allow(artifactory).to receive(:get_authentication_for).and_return(nil)
737
+ invalid_uri = URI('https://example.com/repo-name/file.txt')
738
+
739
+ expect {
740
+ artifactory.upload(local_file, invalid_uri)
741
+ }.to raise_error(ArgumentError, /URI must contain 'artifactory' in path/)
587
742
  end
743
+ end
588
744
 
589
- artifactory.upload(local_file, uri)
745
+ context 'metadata collection' do
746
+ let(:artifactory) { Fig::Protocol::Artifactory.new nil }
590
747
 
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.'))
748
+ it 'collects metadata with fig. prefix' do
749
+ allow(artifactory).to receive(:get_authentication_for).and_return(nil)
750
+ config_mock = double('config')
751
+ allow(config_mock).to receive(:endpoint=)
752
+ allow(::Artifactory).to receive(:configure).and_yield(config_mock)
753
+ metadata_hash = nil
754
+ allow(mock_artifact).to receive(:upload) do |repo, path, metadata|
755
+ metadata_hash = metadata
756
+ end
757
+
758
+ artifactory.upload(local_file, uri)
759
+
760
+ expect(metadata_hash).to include(
761
+ 'fig.original_path' => local_file,
762
+ 'fig.target_path' => 'path/to/file.txt',
763
+ 'fig.tool' => 'fig-artifactory-protocol'
764
+ )
765
+ expect(metadata_hash.keys).to all(start_with('fig.'))
766
+ end
597
767
  end
598
768
  end
599
769
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fig
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0
4
+ version: 2.1.0.pre.alpha.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Fig Folks
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-10-14 00:00:00.000000000 Z
10
+ date: 2025-10-20 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: bcrypt_pbkdf
@@ -334,7 +334,7 @@ dependencies:
334
334
  description: |-
335
335
  Fig is a utility for configuring environments and managing dependencies across a team of developers. Given a list of packages and a command to run, Fig builds environment variables named in those packages (e.g., CLASSPATH), then executes the command in that environment. The caller's environment is not affected.
336
336
 
337
- Built from git SHA1: f9ab9a8-dirty
337
+ Built from git SHA1: 3d9b2c6-dirty
338
338
  email: maintainer@figpackagemanager.org
339
339
  executables:
340
340
  - fig
@@ -547,7 +547,7 @@ files:
547
547
  licenses:
548
548
  - BSD-3-Clause
549
549
  metadata:
550
- git_sha: f9ab9a8-dirty
550
+ git_sha: 3d9b2c6-dirty
551
551
  rdoc_options: []
552
552
  require_paths:
553
553
  - lib
@@ -565,5 +565,5 @@ requirements: []
565
565
  rubygems_version: 3.6.1
566
566
  specification_version: 4
567
567
  summary: 'Utility for configuring environments and managing dependencies across a
568
- team of developers. Built from git SHA1: f9ab9a8-dirty'
568
+ team of developers. Built from git SHA1: 3d9b2c6-dirty'
569
569
  test_files: []