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.
- checksums.yaml +4 -4
- data/lib/fig/operating_system.rb +3 -0
- data/lib/fig/protocol/artifactory.rb +389 -0
- data/lib/fig/spec_utils.rb +312 -0
- data/lib/fig/version.rb +1 -1
- data/spec/application_configuration_spec.rb +73 -0
- data/spec/command/clean_spec.rb +62 -0
- data/spec/command/command_line_vs_package_spec.rb +32 -0
- data/spec/command/dump_package_definition_spec.rb +104 -0
- data/spec/command/environment_variables_spec.rb +62 -0
- data/spec/command/grammar_asset_spec.rb +391 -0
- data/spec/command/grammar_command_spec.rb +88 -0
- data/spec/command/grammar_environment_variable_spec.rb +384 -0
- data/spec/command/grammar_retrieve_spec.rb +74 -0
- data/spec/command/grammar_spec.rb +87 -0
- data/spec/command/grammar_spec_helper.rb +23 -0
- data/spec/command/include_file_spec.rb +73 -0
- data/spec/command/listing_spec.rb +1574 -0
- data/spec/command/miscellaneous_spec.rb +145 -0
- data/spec/command/publish_local_and_updates_spec.rb +32 -0
- data/spec/command/publishing_retrieval_spec.rb +423 -0
- data/spec/command/publishing_spec.rb +596 -0
- data/spec/command/running_commands_spec.rb +354 -0
- data/spec/command/suppress_includes_spec.rb +65 -0
- data/spec/command/suppress_warning_include_statement_missing_version_spec.rb +134 -0
- data/spec/command/update_lock_response_spec.rb +47 -0
- data/spec/command/usage_errors_spec.rb +481 -0
- data/spec/command_options_spec.rb +184 -0
- data/spec/command_spec.rb +49 -0
- data/spec/deparser/v1_spec.rb +64 -0
- data/spec/environment_variables_spec.rb +91 -0
- data/spec/figrc_spec.rb +144 -0
- data/spec/parser_spec.rb +398 -0
- data/spec/protocol/artifactory_spec.rb +599 -0
- data/spec/repository_spec.rb +117 -0
- data/spec/runtime_environment_spec.rb +357 -0
- data/spec/spec_helper.rb +1 -0
- data/spec/split_repo_url_spec.rb +190 -0
- data/spec/statement/asset_spec.rb +203 -0
- data/spec/statement/configuration_spec.rb +41 -0
- data/spec/support/formatters/seed_spitter.rb +12 -0
- data/spec/working_directory_maintainer_spec.rb +102 -0
- 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
|