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