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 +4 -4
- data/lib/fig/operating_system.rb +1 -1
- data/lib/fig/protocol/artifactory.rb +43 -29
- data/lib/fig/version.rb +4 -1
- data/spec/protocol/artifactory_spec.rb +268 -98
- metadata +5 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a99ef226fefef4e3851e250d0ce01dacb7c4fc3bf9408ec697ea11a88b3ba471
|
|
4
|
+
data.tar.gz: f1ce4ec0076a2b2e5de61e4fbe52fb93de1c00ff9cb4427e3fb21e14a7f39a8e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d680a96c48d21158c472fc40a9f1141a6aac72353d1c456e2a5a8a763c12b05fe63c6e891699aba27525492a37ab8c6ce76271f8f2156cbdf85f3c72eddbcf19
|
|
7
|
+
data.tar.gz: c11a5555b761e03be9b6367f98359cbbb1f1839b2d83237aff55222f984f6dc8b1483b18be9978cdcf7f99b9cf5276316d9fc9b852c2ffb72c6b79d61829105f
|
data/lib/fig/operating_system.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
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
|
|
132
|
-
Fig::Logging.debug("Equivalent curl: curl -u #{
|
|
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
|
-
|
|
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
|
-
#
|
|
367
|
-
|
|
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
|
-
|
|
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
|
@@ -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 '
|
|
130
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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
|
-
|
|
402
|
+
expect(Fig::Logging).to receive(:debug).with(/#{Regexp.escape(scenario[:curl])}.*#{Regexp.escape(path)}.*#{Regexp.escape(https_uri.to_s)}/)
|
|
293
403
|
|
|
294
|
-
|
|
295
|
-
|
|
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 '
|
|
300
|
-
|
|
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
|
-
|
|
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
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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
|
-
|
|
328
|
-
|
|
441
|
+
expect(::File).to receive(:open).with(path, 'wb')
|
|
442
|
+
expect(mock_file).to receive(:binmode)
|
|
329
443
|
|
|
330
|
-
|
|
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(:
|
|
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
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
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
|
-
|
|
560
|
-
|
|
690
|
+
before do
|
|
691
|
+
allow(artifactory).to receive(:get_authentication_for).and_return(auth)
|
|
692
|
+
end
|
|
561
693
|
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
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
|
-
|
|
576
|
-
|
|
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
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
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
|
-
|
|
745
|
+
context 'metadata collection' do
|
|
746
|
+
let(:artifactory) { Fig::Protocol::Artifactory.new nil }
|
|
590
747
|
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
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.
|
|
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-
|
|
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:
|
|
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:
|
|
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:
|
|
568
|
+
team of developers. Built from git SHA1: 3d9b2c6-dirty'
|
|
569
569
|
test_files: []
|