1# -*- coding: utf-8 -*-
2# Copyright: (c) 2019, Ansible Project
3# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
4
5# Make coding more python3-ish
6from __future__ import (absolute_import, division, print_function)
7__metaclass__ = type
8
9import json
10import os
11import re
12import pytest
13import tarfile
14import tempfile
15import time
16
17from io import BytesIO, StringIO
18from units.compat.mock import MagicMock
19
20from ansible import context
21from ansible.errors import AnsibleError
22from ansible.galaxy import api as galaxy_api
23from ansible.galaxy.api import CollectionVersionMetadata, GalaxyAPI, GalaxyError
24from ansible.galaxy.token import BasicAuthToken, GalaxyToken, KeycloakToken
25from ansible.module_utils._text import to_native, to_text
26from ansible.module_utils.six.moves.urllib import error as urllib_error
27from ansible.utils import context_objects as co
28from ansible.utils.display import Display
29
30
31@pytest.fixture(autouse='function')
32def reset_cli_args():
33    co.GlobalCLIArgs._Singleton__instance = None
34    # Required to initialise the GalaxyAPI object
35    context.CLIARGS._store = {'ignore_certs': False}
36    yield
37    co.GlobalCLIArgs._Singleton__instance = None
38
39
40@pytest.fixture()
41def collection_artifact(tmp_path_factory):
42    ''' Creates a collection artifact tarball that is ready to be published '''
43    output_dir = to_text(tmp_path_factory.mktemp('test-ÅÑŚÌβŁÈ Output'))
44
45    tar_path = os.path.join(output_dir, 'namespace-collection-v1.0.0.tar.gz')
46    with tarfile.open(tar_path, 'w:gz') as tfile:
47        b_io = BytesIO(b"\x00\x01\x02\x03")
48        tar_info = tarfile.TarInfo('test')
49        tar_info.size = 4
50        tar_info.mode = 0o0644
51        tfile.addfile(tarinfo=tar_info, fileobj=b_io)
52
53    yield tar_path
54
55
56def get_test_galaxy_api(url, version, token_ins=None, token_value=None):
57    token_value = token_value or "my token"
58    token_ins = token_ins or GalaxyToken(token_value)
59    api = GalaxyAPI(None, "test", url)
60    # Warning, this doesn't test g_connect() because _availabe_api_versions is set here.  That means
61    # that urls for v2 servers have to append '/api/' themselves in the input data.
62    api._available_api_versions = {version: '%s' % version}
63    api.token = token_ins
64
65    return api
66
67
68def test_api_no_auth():
69    api = GalaxyAPI(None, "test", "https://galaxy.ansible.com/api/")
70    actual = {}
71    api._add_auth_token(actual, "")
72    assert actual == {}
73
74
75def test_api_no_auth_but_required():
76    expected = "No access token or username set. A token can be set with --api-key or at "
77    with pytest.raises(AnsibleError, match=expected):
78        GalaxyAPI(None, "test", "https://galaxy.ansible.com/api/")._add_auth_token({}, "", required=True)
79
80
81def test_api_token_auth():
82    token = GalaxyToken(token=u"my_token")
83    api = GalaxyAPI(None, "test", "https://galaxy.ansible.com/api/", token=token)
84    actual = {}
85    api._add_auth_token(actual, "", required=True)
86    assert actual == {'Authorization': 'Token my_token'}
87
88
89def test_api_token_auth_with_token_type(monkeypatch):
90    token = KeycloakToken(auth_url='https://api.test/')
91    mock_token_get = MagicMock()
92    mock_token_get.return_value = 'my_token'
93    monkeypatch.setattr(token, 'get', mock_token_get)
94    api = GalaxyAPI(None, "test", "https://galaxy.ansible.com/api/", token=token)
95    actual = {}
96    api._add_auth_token(actual, "", token_type="Bearer", required=True)
97    assert actual == {'Authorization': 'Bearer my_token'}
98
99
100def test_api_token_auth_with_v3_url(monkeypatch):
101    token = KeycloakToken(auth_url='https://api.test/')
102    mock_token_get = MagicMock()
103    mock_token_get.return_value = 'my_token'
104    monkeypatch.setattr(token, 'get', mock_token_get)
105    api = GalaxyAPI(None, "test", "https://galaxy.ansible.com/api/", token=token)
106    actual = {}
107    api._add_auth_token(actual, "https://galaxy.ansible.com/api/v3/resource/name", required=True)
108    assert actual == {'Authorization': 'Bearer my_token'}
109
110
111def test_api_token_auth_with_v2_url():
112    token = GalaxyToken(token=u"my_token")
113    api = GalaxyAPI(None, "test", "https://galaxy.ansible.com/api/", token=token)
114    actual = {}
115    # Add v3 to random part of URL but response should only see the v2 as the full URI path segment.
116    api._add_auth_token(actual, "https://galaxy.ansible.com/api/v2/resourcev3/name", required=True)
117    assert actual == {'Authorization': 'Token my_token'}
118
119
120def test_api_basic_auth_password():
121    token = BasicAuthToken(username=u"user", password=u"pass")
122    api = GalaxyAPI(None, "test", "https://galaxy.ansible.com/api/", token=token)
123    actual = {}
124    api._add_auth_token(actual, "", required=True)
125    assert actual == {'Authorization': 'Basic dXNlcjpwYXNz'}
126
127
128def test_api_basic_auth_no_password():
129    token = BasicAuthToken(username=u"user")
130    api = GalaxyAPI(None, "test", "https://galaxy.ansible.com/api/", token=token)
131    actual = {}
132    api._add_auth_token(actual, "", required=True)
133    assert actual == {'Authorization': 'Basic dXNlcjo='}
134
135
136def test_api_dont_override_auth_header():
137    api = GalaxyAPI(None, "test", "https://galaxy.ansible.com/api/")
138    actual = {'Authorization': 'Custom token'}
139    api._add_auth_token(actual, "", required=True)
140    assert actual == {'Authorization': 'Custom token'}
141
142
143def test_initialise_galaxy(monkeypatch):
144    mock_open = MagicMock()
145    mock_open.side_effect = [
146        StringIO(u'{"available_versions":{"v1":"v1/"}}'),
147        StringIO(u'{"token":"my token"}'),
148    ]
149    monkeypatch.setattr(galaxy_api, 'open_url', mock_open)
150
151    api = GalaxyAPI(None, "test", "https://galaxy.ansible.com/api/")
152    actual = api.authenticate("github_token")
153
154    assert len(api.available_api_versions) == 2
155    assert api.available_api_versions['v1'] == u'v1/'
156    assert api.available_api_versions['v2'] == u'v2/'
157    assert actual == {u'token': u'my token'}
158    assert mock_open.call_count == 2
159    assert mock_open.mock_calls[0][1][0] == 'https://galaxy.ansible.com/api/'
160    assert 'ansible-galaxy' in mock_open.mock_calls[0][2]['http_agent']
161    assert mock_open.mock_calls[1][1][0] == 'https://galaxy.ansible.com/api/v1/tokens/'
162    assert 'ansible-galaxy' in mock_open.mock_calls[1][2]['http_agent']
163    assert mock_open.mock_calls[1][2]['data'] == 'github_token=github_token'
164
165
166def test_initialise_galaxy_with_auth(monkeypatch):
167    mock_open = MagicMock()
168    mock_open.side_effect = [
169        StringIO(u'{"available_versions":{"v1":"v1/"}}'),
170        StringIO(u'{"token":"my token"}'),
171    ]
172    monkeypatch.setattr(galaxy_api, 'open_url', mock_open)
173
174    api = GalaxyAPI(None, "test", "https://galaxy.ansible.com/api/", token=GalaxyToken(token='my_token'))
175    actual = api.authenticate("github_token")
176
177    assert len(api.available_api_versions) == 2
178    assert api.available_api_versions['v1'] == u'v1/'
179    assert api.available_api_versions['v2'] == u'v2/'
180    assert actual == {u'token': u'my token'}
181    assert mock_open.call_count == 2
182    assert mock_open.mock_calls[0][1][0] == 'https://galaxy.ansible.com/api/'
183    assert 'ansible-galaxy' in mock_open.mock_calls[0][2]['http_agent']
184    assert mock_open.mock_calls[1][1][0] == 'https://galaxy.ansible.com/api/v1/tokens/'
185    assert 'ansible-galaxy' in mock_open.mock_calls[1][2]['http_agent']
186    assert mock_open.mock_calls[1][2]['data'] == 'github_token=github_token'
187
188
189def test_initialise_automation_hub(monkeypatch):
190    mock_open = MagicMock()
191    mock_open.side_effect = [
192        StringIO(u'{"available_versions":{"v2": "v2/", "v3":"v3/"}}'),
193    ]
194    monkeypatch.setattr(galaxy_api, 'open_url', mock_open)
195    token = KeycloakToken(auth_url='https://api.test/')
196    mock_token_get = MagicMock()
197    mock_token_get.return_value = 'my_token'
198    monkeypatch.setattr(token, 'get', mock_token_get)
199
200    api = GalaxyAPI(None, "test", "https://galaxy.ansible.com/api/", token=token)
201
202    assert len(api.available_api_versions) == 2
203    assert api.available_api_versions['v2'] == u'v2/'
204    assert api.available_api_versions['v3'] == u'v3/'
205
206    assert mock_open.mock_calls[0][1][0] == 'https://galaxy.ansible.com/api/'
207    assert 'ansible-galaxy' in mock_open.mock_calls[0][2]['http_agent']
208    assert mock_open.mock_calls[0][2]['headers'] == {'Authorization': 'Bearer my_token'}
209
210
211def test_initialise_unknown(monkeypatch):
212    mock_open = MagicMock()
213    mock_open.side_effect = [
214        urllib_error.HTTPError('https://galaxy.ansible.com/api/', 500, 'msg', {}, StringIO(u'{"msg":"raw error"}')),
215        urllib_error.HTTPError('https://galaxy.ansible.com/api/api/', 500, 'msg', {}, StringIO(u'{"msg":"raw error"}')),
216    ]
217    monkeypatch.setattr(galaxy_api, 'open_url', mock_open)
218
219    api = GalaxyAPI(None, "test", "https://galaxy.ansible.com/api/", token=GalaxyToken(token='my_token'))
220
221    expected = "Error when finding available api versions from test (%s) (HTTP Code: 500, Message: msg)" \
222        % api.api_server
223    with pytest.raises(AnsibleError, match=re.escape(expected)):
224        api.authenticate("github_token")
225
226
227def test_get_available_api_versions(monkeypatch):
228    mock_open = MagicMock()
229    mock_open.side_effect = [
230        StringIO(u'{"available_versions":{"v1":"v1/","v2":"v2/"}}'),
231    ]
232    monkeypatch.setattr(galaxy_api, 'open_url', mock_open)
233
234    api = GalaxyAPI(None, "test", "https://galaxy.ansible.com/api/")
235    actual = api.available_api_versions
236    assert len(actual) == 2
237    assert actual['v1'] == u'v1/'
238    assert actual['v2'] == u'v2/'
239
240    assert mock_open.call_count == 1
241    assert mock_open.mock_calls[0][1][0] == 'https://galaxy.ansible.com/api/'
242    assert 'ansible-galaxy' in mock_open.mock_calls[0][2]['http_agent']
243
244
245def test_publish_collection_missing_file():
246    fake_path = u'/fake/ÅÑŚÌβŁÈ/path'
247    expected = to_native("The collection path specified '%s' does not exist." % fake_path)
248
249    api = get_test_galaxy_api("https://galaxy.ansible.com/api/", "v2")
250    with pytest.raises(AnsibleError, match=expected):
251        api.publish_collection(fake_path)
252
253
254def test_publish_collection_not_a_tarball():
255    expected = "The collection path specified '{0}' is not a tarball, use 'ansible-galaxy collection build' to " \
256               "create a proper release artifact."
257
258    api = get_test_galaxy_api("https://galaxy.ansible.com/api/", "v2")
259    with tempfile.NamedTemporaryFile(prefix=u'ÅÑŚÌβŁÈ') as temp_file:
260        temp_file.write(b"\x00")
261        temp_file.flush()
262        with pytest.raises(AnsibleError, match=expected.format(to_native(temp_file.name))):
263            api.publish_collection(temp_file.name)
264
265
266def test_publish_collection_unsupported_version():
267    expected = "Galaxy action publish_collection requires API versions 'v2, v3' but only 'v1' are available on test " \
268               "https://galaxy.ansible.com/api/"
269
270    api = get_test_galaxy_api("https://galaxy.ansible.com/api/", "v1")
271    with pytest.raises(AnsibleError, match=expected):
272        api.publish_collection("path")
273
274
275@pytest.mark.parametrize('api_version, collection_url', [
276    ('v2', 'collections'),
277    ('v3', 'artifacts/collections'),
278])
279def test_publish_collection(api_version, collection_url, collection_artifact, monkeypatch):
280    api = get_test_galaxy_api("https://galaxy.ansible.com/api/", api_version)
281
282    mock_call = MagicMock()
283    mock_call.return_value = {'task': 'http://task.url/'}
284    monkeypatch.setattr(api, '_call_galaxy', mock_call)
285
286    actual = api.publish_collection(collection_artifact)
287    assert actual == 'http://task.url/'
288    assert mock_call.call_count == 1
289    assert mock_call.mock_calls[0][1][0] == 'https://galaxy.ansible.com/api/%s/%s/' % (api_version, collection_url)
290    assert mock_call.mock_calls[0][2]['headers']['Content-length'] == len(mock_call.mock_calls[0][2]['args'])
291    assert mock_call.mock_calls[0][2]['headers']['Content-type'].startswith(
292        'multipart/form-data; boundary=')
293    assert mock_call.mock_calls[0][2]['args'].startswith(b'--')
294    assert mock_call.mock_calls[0][2]['method'] == 'POST'
295    assert mock_call.mock_calls[0][2]['auth_required'] is True
296
297
298@pytest.mark.parametrize('api_version, collection_url, response, expected', [
299    ('v2', 'collections', {},
300     'Error when publishing collection to test (%s) (HTTP Code: 500, Message: msg Code: Unknown)'),
301    ('v2', 'collections', {
302        'message': u'Galaxy error messäge',
303        'code': 'GWE002',
304    }, u'Error when publishing collection to test (%s) (HTTP Code: 500, Message: Galaxy error messäge Code: GWE002)'),
305    ('v3', 'artifact/collections', {},
306     'Error when publishing collection to test (%s) (HTTP Code: 500, Message: msg Code: Unknown)'),
307    ('v3', 'artifact/collections', {
308        'errors': [
309            {
310                'code': 'conflict.collection_exists',
311                'detail': 'Collection "mynamespace-mycollection-4.1.1" already exists.',
312                'title': 'Conflict.',
313                'status': '400',
314            },
315            {
316                'code': 'quantum_improbability',
317                'title': u'Rändom(?) quantum improbability.',
318                'source': {'parameter': 'the_arrow_of_time'},
319                'meta': {'remediation': 'Try again before'},
320            },
321        ],
322    }, u'Error when publishing collection to test (%s) (HTTP Code: 500, Message: Collection '
323       u'"mynamespace-mycollection-4.1.1" already exists. Code: conflict.collection_exists), (HTTP Code: 500, '
324       u'Message: Rändom(?) quantum improbability. Code: quantum_improbability)')
325])
326def test_publish_failure(api_version, collection_url, response, expected, collection_artifact, monkeypatch):
327    api = get_test_galaxy_api('https://galaxy.server.com/api/', api_version)
328
329    expected_url = '%s/api/%s/%s' % (api.api_server, api_version, collection_url)
330
331    mock_open = MagicMock()
332    mock_open.side_effect = urllib_error.HTTPError(expected_url, 500, 'msg', {},
333                                                   StringIO(to_text(json.dumps(response))))
334    monkeypatch.setattr(galaxy_api, 'open_url', mock_open)
335
336    with pytest.raises(GalaxyError, match=re.escape(to_native(expected % api.api_server))):
337        api.publish_collection(collection_artifact)
338
339
340@pytest.mark.parametrize('server_url, api_version, token_type, token_ins, import_uri, full_import_uri', [
341    ('https://galaxy.server.com/api', 'v2', 'Token', GalaxyToken('my token'),
342     '1234',
343     'https://galaxy.server.com/api/v2/collection-imports/1234/'),
344    ('https://galaxy.server.com/api/automation-hub/', 'v3', 'Bearer', KeycloakToken(auth_url='https://api.test/'),
345     '1234',
346     'https://galaxy.server.com/api/automation-hub/v3/imports/collections/1234/'),
347])
348def test_wait_import_task(server_url, api_version, token_type, token_ins, import_uri, full_import_uri, monkeypatch):
349    api = get_test_galaxy_api(server_url, api_version, token_ins=token_ins)
350
351    if token_ins:
352        mock_token_get = MagicMock()
353        mock_token_get.return_value = 'my token'
354        monkeypatch.setattr(token_ins, 'get', mock_token_get)
355
356    mock_open = MagicMock()
357    mock_open.return_value = StringIO(u'{"state":"success","finished_at":"time"}')
358    monkeypatch.setattr(galaxy_api, 'open_url', mock_open)
359
360    mock_display = MagicMock()
361    monkeypatch.setattr(Display, 'display', mock_display)
362
363    api.wait_import_task(import_uri)
364
365    assert mock_open.call_count == 1
366    assert mock_open.mock_calls[0][1][0] == full_import_uri
367    assert mock_open.mock_calls[0][2]['headers']['Authorization'] == '%s my token' % token_type
368
369    assert mock_display.call_count == 1
370    assert mock_display.mock_calls[0][1][0] == 'Waiting until Galaxy import task %s has completed' % full_import_uri
371
372
373@pytest.mark.parametrize('server_url, api_version, token_type, token_ins, import_uri, full_import_uri', [
374    ('https://galaxy.server.com/api/', 'v2', 'Token', GalaxyToken('my token'),
375     '1234',
376     'https://galaxy.server.com/api/v2/collection-imports/1234/'),
377    ('https://galaxy.server.com/api/automation-hub', 'v3', 'Bearer', KeycloakToken(auth_url='https://api.test/'),
378     '1234',
379     'https://galaxy.server.com/api/automation-hub/v3/imports/collections/1234/'),
380])
381def test_wait_import_task_multiple_requests(server_url, api_version, token_type, token_ins, import_uri, full_import_uri, monkeypatch):
382    api = get_test_galaxy_api(server_url, api_version, token_ins=token_ins)
383
384    if token_ins:
385        mock_token_get = MagicMock()
386        mock_token_get.return_value = 'my token'
387        monkeypatch.setattr(token_ins, 'get', mock_token_get)
388
389    mock_open = MagicMock()
390    mock_open.side_effect = [
391        StringIO(u'{"state":"test"}'),
392        StringIO(u'{"state":"success","finished_at":"time"}'),
393    ]
394    monkeypatch.setattr(galaxy_api, 'open_url', mock_open)
395
396    mock_display = MagicMock()
397    monkeypatch.setattr(Display, 'display', mock_display)
398
399    mock_vvv = MagicMock()
400    monkeypatch.setattr(Display, 'vvv', mock_vvv)
401
402    monkeypatch.setattr(time, 'sleep', MagicMock())
403
404    api.wait_import_task(import_uri)
405
406    assert mock_open.call_count == 2
407    assert mock_open.mock_calls[0][1][0] == full_import_uri
408    assert mock_open.mock_calls[0][2]['headers']['Authorization'] == '%s my token' % token_type
409    assert mock_open.mock_calls[1][1][0] == full_import_uri
410    assert mock_open.mock_calls[1][2]['headers']['Authorization'] == '%s my token' % token_type
411
412    assert mock_display.call_count == 1
413    assert mock_display.mock_calls[0][1][0] == 'Waiting until Galaxy import task %s has completed' % full_import_uri
414
415    assert mock_vvv.call_count == 1
416    assert mock_vvv.mock_calls[0][1][0] == \
417        'Galaxy import process has a status of test, wait 2 seconds before trying again'
418
419
420@pytest.mark.parametrize('server_url, api_version, token_type, token_ins, import_uri, full_import_uri,', [
421    ('https://galaxy.server.com/api/', 'v2', 'Token', GalaxyToken('my token'),
422     '1234',
423     'https://galaxy.server.com/api/v2/collection-imports/1234/'),
424    ('https://galaxy.server.com/api/automation-hub/', 'v3', 'Bearer', KeycloakToken(auth_url='https://api.test/'),
425     '1234',
426     'https://galaxy.server.com/api/automation-hub/v3/imports/collections/1234/'),
427])
428def test_wait_import_task_with_failure(server_url, api_version, token_type, token_ins, import_uri, full_import_uri, monkeypatch):
429    api = get_test_galaxy_api(server_url, api_version, token_ins=token_ins)
430
431    if token_ins:
432        mock_token_get = MagicMock()
433        mock_token_get.return_value = 'my token'
434        monkeypatch.setattr(token_ins, 'get', mock_token_get)
435
436    mock_open = MagicMock()
437    mock_open.side_effect = [
438        StringIO(to_text(json.dumps({
439            'finished_at': 'some_time',
440            'state': 'failed',
441            'error': {
442                'code': 'GW001',
443                'description': u'Becäuse I said so!',
444
445            },
446            'messages': [
447                {
448                    'level': 'error',
449                    'message': u'Somé error',
450                },
451                {
452                    'level': 'warning',
453                    'message': u'Some wärning',
454                },
455                {
456                    'level': 'info',
457                    'message': u'Somé info',
458                },
459            ],
460        }))),
461    ]
462    monkeypatch.setattr(galaxy_api, 'open_url', mock_open)
463
464    mock_display = MagicMock()
465    monkeypatch.setattr(Display, 'display', mock_display)
466
467    mock_vvv = MagicMock()
468    monkeypatch.setattr(Display, 'vvv', mock_vvv)
469
470    mock_warn = MagicMock()
471    monkeypatch.setattr(Display, 'warning', mock_warn)
472
473    mock_err = MagicMock()
474    monkeypatch.setattr(Display, 'error', mock_err)
475
476    expected = to_native(u'Galaxy import process failed: Becäuse I said so! (Code: GW001)')
477    with pytest.raises(AnsibleError, match=re.escape(expected)):
478        api.wait_import_task(import_uri)
479
480    assert mock_open.call_count == 1
481    assert mock_open.mock_calls[0][1][0] == full_import_uri
482    assert mock_open.mock_calls[0][2]['headers']['Authorization'] == '%s my token' % token_type
483
484    assert mock_display.call_count == 1
485    assert mock_display.mock_calls[0][1][0] == 'Waiting until Galaxy import task %s has completed' % full_import_uri
486
487    assert mock_vvv.call_count == 1
488    assert mock_vvv.mock_calls[0][1][0] == u'Galaxy import message: info - Somé info'
489
490    assert mock_warn.call_count == 1
491    assert mock_warn.mock_calls[0][1][0] == u'Galaxy import warning message: Some wärning'
492
493    assert mock_err.call_count == 1
494    assert mock_err.mock_calls[0][1][0] == u'Galaxy import error message: Somé error'
495
496
497@pytest.mark.parametrize('server_url, api_version, token_type, token_ins, import_uri, full_import_uri', [
498    ('https://galaxy.server.com/api/', 'v2', 'Token', GalaxyToken('my_token'),
499     '1234',
500     'https://galaxy.server.com/api/v2/collection-imports/1234/'),
501    ('https://galaxy.server.com/api/automation-hub/', 'v3', 'Bearer', KeycloakToken(auth_url='https://api.test/'),
502     '1234',
503     'https://galaxy.server.com/api/automation-hub/v3/imports/collections/1234/'),
504])
505def test_wait_import_task_with_failure_no_error(server_url, api_version, token_type, token_ins, import_uri, full_import_uri, monkeypatch):
506    api = get_test_galaxy_api(server_url, api_version, token_ins=token_ins)
507
508    if token_ins:
509        mock_token_get = MagicMock()
510        mock_token_get.return_value = 'my token'
511        monkeypatch.setattr(token_ins, 'get', mock_token_get)
512
513    mock_open = MagicMock()
514    mock_open.side_effect = [
515        StringIO(to_text(json.dumps({
516            'finished_at': 'some_time',
517            'state': 'failed',
518            'error': {},
519            'messages': [
520                {
521                    'level': 'error',
522                    'message': u'Somé error',
523                },
524                {
525                    'level': 'warning',
526                    'message': u'Some wärning',
527                },
528                {
529                    'level': 'info',
530                    'message': u'Somé info',
531                },
532            ],
533        }))),
534    ]
535    monkeypatch.setattr(galaxy_api, 'open_url', mock_open)
536
537    mock_display = MagicMock()
538    monkeypatch.setattr(Display, 'display', mock_display)
539
540    mock_vvv = MagicMock()
541    monkeypatch.setattr(Display, 'vvv', mock_vvv)
542
543    mock_warn = MagicMock()
544    monkeypatch.setattr(Display, 'warning', mock_warn)
545
546    mock_err = MagicMock()
547    monkeypatch.setattr(Display, 'error', mock_err)
548
549    expected = 'Galaxy import process failed: Unknown error, see %s for more details \\(Code: UNKNOWN\\)' % full_import_uri
550    with pytest.raises(AnsibleError, match=expected):
551        api.wait_import_task(import_uri)
552
553    assert mock_open.call_count == 1
554    assert mock_open.mock_calls[0][1][0] == full_import_uri
555    assert mock_open.mock_calls[0][2]['headers']['Authorization'] == '%s my token' % token_type
556
557    assert mock_display.call_count == 1
558    assert mock_display.mock_calls[0][1][0] == 'Waiting until Galaxy import task %s has completed' % full_import_uri
559
560    assert mock_vvv.call_count == 1
561    assert mock_vvv.mock_calls[0][1][0] == u'Galaxy import message: info - Somé info'
562
563    assert mock_warn.call_count == 1
564    assert mock_warn.mock_calls[0][1][0] == u'Galaxy import warning message: Some wärning'
565
566    assert mock_err.call_count == 1
567    assert mock_err.mock_calls[0][1][0] == u'Galaxy import error message: Somé error'
568
569
570@pytest.mark.parametrize('server_url, api_version, token_type, token_ins, import_uri, full_import_uri', [
571    ('https://galaxy.server.com/api', 'v2', 'Token', GalaxyToken('my token'),
572     '1234',
573     'https://galaxy.server.com/api/v2/collection-imports/1234/'),
574    ('https://galaxy.server.com/api/automation-hub', 'v3', 'Bearer', KeycloakToken(auth_url='https://api.test/'),
575     '1234',
576     'https://galaxy.server.com/api/automation-hub/v3/imports/collections/1234/'),
577])
578def test_wait_import_task_timeout(server_url, api_version, token_type, token_ins, import_uri, full_import_uri, monkeypatch):
579    api = get_test_galaxy_api(server_url, api_version, token_ins=token_ins)
580
581    if token_ins:
582        mock_token_get = MagicMock()
583        mock_token_get.return_value = 'my token'
584        monkeypatch.setattr(token_ins, 'get', mock_token_get)
585
586    def return_response(*args, **kwargs):
587        return StringIO(u'{"state":"waiting"}')
588
589    mock_open = MagicMock()
590    mock_open.side_effect = return_response
591    monkeypatch.setattr(galaxy_api, 'open_url', mock_open)
592
593    mock_display = MagicMock()
594    monkeypatch.setattr(Display, 'display', mock_display)
595
596    mock_vvv = MagicMock()
597    monkeypatch.setattr(Display, 'vvv', mock_vvv)
598
599    monkeypatch.setattr(time, 'sleep', MagicMock())
600
601    expected = "Timeout while waiting for the Galaxy import process to finish, check progress at '%s'" % full_import_uri
602    with pytest.raises(AnsibleError, match=expected):
603        api.wait_import_task(import_uri, 1)
604
605    assert mock_open.call_count > 1
606    assert mock_open.mock_calls[0][1][0] == full_import_uri
607    assert mock_open.mock_calls[0][2]['headers']['Authorization'] == '%s my token' % token_type
608    assert mock_open.mock_calls[1][1][0] == full_import_uri
609    assert mock_open.mock_calls[1][2]['headers']['Authorization'] == '%s my token' % token_type
610
611    assert mock_display.call_count == 1
612    assert mock_display.mock_calls[0][1][0] == 'Waiting until Galaxy import task %s has completed' % full_import_uri
613
614    # expected_wait_msg = 'Galaxy import process has a status of waiting, wait {0} seconds before trying again'
615    assert mock_vvv.call_count > 9  # 1st is opening Galaxy token file.
616
617    # FIXME:
618    # assert mock_vvv.mock_calls[1][1][0] == expected_wait_msg.format(2)
619    # assert mock_vvv.mock_calls[2][1][0] == expected_wait_msg.format(3)
620    # assert mock_vvv.mock_calls[3][1][0] == expected_wait_msg.format(4)
621    # assert mock_vvv.mock_calls[4][1][0] == expected_wait_msg.format(6)
622    # assert mock_vvv.mock_calls[5][1][0] == expected_wait_msg.format(10)
623    # assert mock_vvv.mock_calls[6][1][0] == expected_wait_msg.format(15)
624    # assert mock_vvv.mock_calls[7][1][0] == expected_wait_msg.format(22)
625    # assert mock_vvv.mock_calls[8][1][0] == expected_wait_msg.format(30)
626
627
628@pytest.mark.parametrize('api_version, token_type, version, token_ins', [
629    ('v2', None, 'v2.1.13', None),
630    ('v3', 'Bearer', 'v1.0.0', KeycloakToken(auth_url='https://api.test/api/automation-hub/')),
631])
632def test_get_collection_version_metadata_no_version(api_version, token_type, version, token_ins, monkeypatch):
633    api = get_test_galaxy_api('https://galaxy.server.com/api/', api_version, token_ins=token_ins)
634
635    if token_ins:
636        mock_token_get = MagicMock()
637        mock_token_get.return_value = 'my token'
638        monkeypatch.setattr(token_ins, 'get', mock_token_get)
639
640    mock_open = MagicMock()
641    mock_open.side_effect = [
642        StringIO(to_text(json.dumps({
643            'download_url': 'https://downloadme.com',
644            'artifact': {
645                'sha256': 'ac47b6fac117d7c171812750dacda655b04533cf56b31080b82d1c0db3c9d80f',
646            },
647            'namespace': {
648                'name': 'namespace',
649            },
650            'collection': {
651                'name': 'collection',
652            },
653            'version': version,
654            'metadata': {
655                'dependencies': {},
656            }
657        }))),
658    ]
659    monkeypatch.setattr(galaxy_api, 'open_url', mock_open)
660
661    actual = api.get_collection_version_metadata('namespace', 'collection', version)
662
663    assert isinstance(actual, CollectionVersionMetadata)
664    assert actual.namespace == u'namespace'
665    assert actual.name == u'collection'
666    assert actual.download_url == u'https://downloadme.com'
667    assert actual.artifact_sha256 == u'ac47b6fac117d7c171812750dacda655b04533cf56b31080b82d1c0db3c9d80f'
668    assert actual.version == version
669    assert actual.dependencies == {}
670
671    assert mock_open.call_count == 1
672    assert mock_open.mock_calls[0][1][0] == '%s%s/collections/namespace/collection/versions/%s/' \
673        % (api.api_server, api_version, version)
674
675    # v2 calls dont need auth, so no authz header or token_type
676    if token_type:
677        assert mock_open.mock_calls[0][2]['headers']['Authorization'] == '%s my token' % token_type
678
679
680@pytest.mark.parametrize('api_version, token_type, token_ins, response', [
681    ('v2', None, None, {
682        'count': 2,
683        'next': None,
684        'previous': None,
685        'results': [
686            {
687                'version': '1.0.0',
688                'href': 'https://galaxy.server.com/api/v2/collections/namespace/collection/versions/1.0.0',
689            },
690            {
691                'version': '1.0.1',
692                'href': 'https://galaxy.server.com/api/v2/collections/namespace/collection/versions/1.0.1',
693            },
694        ],
695    }),
696    # TODO: Verify this once Automation Hub is actually out
697    ('v3', 'Bearer', KeycloakToken(auth_url='https://api.test/'), {
698        'count': 2,
699        'next': None,
700        'previous': None,
701        'data': [
702            {
703                'version': '1.0.0',
704                'href': 'https://galaxy.server.com/api/v2/collections/namespace/collection/versions/1.0.0',
705            },
706            {
707                'version': '1.0.1',
708                'href': 'https://galaxy.server.com/api/v2/collections/namespace/collection/versions/1.0.1',
709            },
710        ],
711    }),
712])
713def test_get_collection_versions(api_version, token_type, token_ins, response, monkeypatch):
714    api = get_test_galaxy_api('https://galaxy.server.com/api/', api_version, token_ins=token_ins)
715
716    if token_ins:
717        mock_token_get = MagicMock()
718        mock_token_get.return_value = 'my token'
719        monkeypatch.setattr(token_ins, 'get', mock_token_get)
720
721    mock_open = MagicMock()
722    mock_open.side_effect = [
723        StringIO(to_text(json.dumps(response))),
724    ]
725    monkeypatch.setattr(galaxy_api, 'open_url', mock_open)
726
727    actual = api.get_collection_versions('namespace', 'collection')
728    assert actual == [u'1.0.0', u'1.0.1']
729
730    page_query = '?limit=100' if api_version == 'v3' else '?page_size=100'
731    assert mock_open.call_count == 1
732    assert mock_open.mock_calls[0][1][0] == 'https://galaxy.server.com/api/%s/collections/namespace/collection/' \
733                                            'versions/%s' % (api_version, page_query)
734    if token_ins:
735        assert mock_open.mock_calls[0][2]['headers']['Authorization'] == '%s my token' % token_type
736
737
738@pytest.mark.parametrize('api_version, token_type, token_ins, responses', [
739    ('v2', None, None, [
740        {
741            'count': 6,
742            'next': 'https://galaxy.server.com/api/v2/collections/namespace/collection/versions/?page=2&page_size=100',
743            'previous': None,
744            'results': [  # Pay no mind, using more manageable results than page_size would indicate
745                {
746                    'version': '1.0.0',
747                    'href': 'https://galaxy.server.com/api/v2/collections/namespace/collection/versions/1.0.0',
748                },
749                {
750                    'version': '1.0.1',
751                    'href': 'https://galaxy.server.com/api/v2/collections/namespace/collection/versions/1.0.1',
752                },
753            ],
754        },
755        {
756            'count': 6,
757            'next': 'https://galaxy.server.com/api/v2/collections/namespace/collection/versions/?page=3&page_size=100',
758            'previous': 'https://galaxy.server.com/api/v2/collections/namespace/collection/versions',
759            'results': [
760                {
761                    'version': '1.0.2',
762                    'href': 'https://galaxy.server.com/api/v2/collections/namespace/collection/versions/1.0.2',
763                },
764                {
765                    'version': '1.0.3',
766                    'href': 'https://galaxy.server.com/api/v2/collections/namespace/collection/versions/1.0.3',
767                },
768            ],
769        },
770        {
771            'count': 6,
772            'next': None,
773            'previous': 'https://galaxy.server.com/api/v2/collections/namespace/collection/versions/?page=2&page_size=100',
774            'results': [
775                {
776                    'version': '1.0.4',
777                    'href': 'https://galaxy.server.com/api/v2/collections/namespace/collection/versions/1.0.4',
778                },
779                {
780                    'version': '1.0.5',
781                    'href': 'https://galaxy.server.com/api/v2/collections/namespace/collection/versions/1.0.5',
782                },
783            ],
784        },
785    ]),
786    ('v3', 'Bearer', KeycloakToken(auth_url='https://api.test/'), [
787        {
788            'count': 6,
789            'links': {
790                # v3 links are relative and the limit is included during pagination
791                'next': '/api/v3/collections/namespace/collection/versions/?limit=100&offset=100',
792                'previous': None,
793            },
794            'data': [
795                {
796                    'version': '1.0.0',
797                    'href': '/api/v3/collections/namespace/collection/versions/1.0.0',
798                },
799                {
800                    'version': '1.0.1',
801                    'href': '/api/v3/collections/namespace/collection/versions/1.0.1',
802                },
803            ],
804        },
805        {
806            'count': 6,
807            'links': {
808                'next': '/api/v3/collections/namespace/collection/versions/?limit=100&offset=200',
809                'previous': '/api/v3/collections/namespace/collection/versions',
810            },
811            'data': [
812                {
813                    'version': '1.0.2',
814                    'href': '/api/v3/collections/namespace/collection/versions/1.0.2',
815                },
816                {
817                    'version': '1.0.3',
818                    'href': '/api/v3/collections/namespace/collection/versions/1.0.3',
819                },
820            ],
821        },
822        {
823            'count': 6,
824            'links': {
825                'next': None,
826                'previous': '/api/v3/collections/namespace/collection/versions/?limit=100&offset=100',
827            },
828            'data': [
829                {
830                    'version': '1.0.4',
831                    'href': '/api/v3/collections/namespace/collection/versions/1.0.4',
832                },
833                {
834                    'version': '1.0.5',
835                    'href': '/api/v3/collections/namespace/collection/versions/1.0.5',
836                },
837            ],
838        },
839    ]),
840])
841def test_get_collection_versions_pagination(api_version, token_type, token_ins, responses, monkeypatch):
842    api = get_test_galaxy_api('https://galaxy.server.com/api/', api_version, token_ins=token_ins)
843
844    if token_ins:
845        mock_token_get = MagicMock()
846        mock_token_get.return_value = 'my token'
847        monkeypatch.setattr(token_ins, 'get', mock_token_get)
848
849    mock_open = MagicMock()
850    mock_open.side_effect = [StringIO(to_text(json.dumps(r))) for r in responses]
851    monkeypatch.setattr(galaxy_api, 'open_url', mock_open)
852
853    actual = api.get_collection_versions('namespace', 'collection')
854    assert actual == [u'1.0.0', u'1.0.1', u'1.0.2', u'1.0.3', u'1.0.4', u'1.0.5']
855
856    assert mock_open.call_count == 3
857
858    if api_version == 'v3':
859        query_1 = 'limit=100'
860        query_2 = 'limit=100&offset=100'
861        query_3 = 'limit=100&offset=200'
862    else:
863        query_1 = 'page_size=100'
864        query_2 = 'page=2&page_size=100'
865        query_3 = 'page=3&page_size=100'
866
867    assert mock_open.mock_calls[0][1][0] == 'https://galaxy.server.com/api/%s/collections/namespace/collection/' \
868                                            'versions/?%s' % (api_version, query_1)
869    assert mock_open.mock_calls[1][1][0] == 'https://galaxy.server.com/api/%s/collections/namespace/collection/' \
870                                            'versions/?%s' % (api_version, query_2)
871    assert mock_open.mock_calls[2][1][0] == 'https://galaxy.server.com/api/%s/collections/namespace/collection/' \
872                                            'versions/?%s' % (api_version, query_3)
873
874    if token_type:
875        assert mock_open.mock_calls[0][2]['headers']['Authorization'] == '%s my token' % token_type
876        assert mock_open.mock_calls[1][2]['headers']['Authorization'] == '%s my token' % token_type
877        assert mock_open.mock_calls[2][2]['headers']['Authorization'] == '%s my token' % token_type
878
879
880@pytest.mark.parametrize('responses', [
881    [
882        {
883            'count': 2,
884            'results': [{'name': '3.5.1', }, {'name': '3.5.2'}],
885            'next_link': None,
886            'next': None,
887            'previous_link': None,
888            'previous': None
889        },
890    ],
891    [
892        {
893            'count': 2,
894            'results': [{'name': '3.5.1'}],
895            'next_link': '/api/v1/roles/432/versions/?page=2&page_size=50',
896            'next': '/roles/432/versions/?page=2&page_size=50',
897            'previous_link': None,
898            'previous': None
899        },
900        {
901            'count': 2,
902            'results': [{'name': '3.5.2'}],
903            'next_link': None,
904            'next': None,
905            'previous_link': '/api/v1/roles/432/versions/?&page_size=50',
906            'previous': '/roles/432/versions/?page_size=50',
907        },
908    ]
909])
910def test_get_role_versions_pagination(monkeypatch, responses):
911    api = get_test_galaxy_api('https://galaxy.com/api/', 'v1')
912
913    mock_open = MagicMock()
914    mock_open.side_effect = [StringIO(to_text(json.dumps(r))) for r in responses]
915    monkeypatch.setattr(galaxy_api, 'open_url', mock_open)
916
917    actual = api.fetch_role_related('versions', 432)
918    assert actual == [{'name': '3.5.1'}, {'name': '3.5.2'}]
919
920    assert mock_open.call_count == len(responses)
921
922    assert mock_open.mock_calls[0][1][0] == 'https://galaxy.com/api/v1/roles/432/versions/?page_size=50'
923    if len(responses) == 2:
924        assert mock_open.mock_calls[1][1][0] == 'https://galaxy.com/api/v1/roles/432/versions/?page=2&page_size=50'
925