1# (c) 2018-2021, NetApp, Inc 2# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 3 4''' unit test for ONTAP publickey Ansible module ''' 5 6from __future__ import (absolute_import, division, print_function) 7__metaclass__ = type 8import json 9import pytest 10import sys 11 12from ansible.module_utils import basic 13from ansible.module_utils._text import to_bytes 14from ansible_collections.netapp.ontap.tests.unit.compat.mock import patch 15import ansible_collections.netapp.ontap.plugins.module_utils.netapp as netapp_utils 16 17from ansible_collections.netapp.ontap.plugins.modules.na_ontap_publickey \ 18 import NetAppOntapPublicKey as my_module, main as uut_main # module under test 19 20 21if not netapp_utils.HAS_REQUESTS and sys.version_info < (2, 7): 22 pytestmark = pytest.mark.skip('Skipping Unit Tests on 2.6 as requests is not be available') 23 24 25def set_module_args(args): 26 """prepare arguments so that they will be picked up during module creation""" 27 args = json.dumps({'ANSIBLE_MODULE_ARGS': args}) 28 basic._ANSIBLE_ARGS = to_bytes(args) # pylint: disable=protected-access 29 30 31class AnsibleExitJson(Exception): 32 """Exception class to be raised by module.exit_json and caught by the test case""" 33 34 35class AnsibleFailJson(Exception): 36 """Exception class to be raised by module.fail_json and caught by the test case""" 37 38 39def exit_json(*args, **kwargs): # pylint: disable=unused-argument 40 """function to patch over exit_json; package return data into an exception""" 41 if 'changed' not in kwargs: 42 kwargs['changed'] = False 43 raise AnsibleExitJson(kwargs) 44 45 46def fail_json(*args, **kwargs): # pylint: disable=unused-argument 47 """function to patch over fail_json; package return data into an exception""" 48 kwargs['failed'] = True 49 raise AnsibleFailJson(kwargs) 50 51 52WARNINGS = list() 53 54 55def warn(dummy, msg): 56 WARNINGS.append(msg) 57 58 59def default_args(): 60 args = { 61 'state': 'present', 62 'hostname': '10.10.10.10', 63 'username': 'admin', 64 'https': 'true', 65 'validate_certs': 'false', 66 'password': 'password', 67 'account': 'user123', 68 'public_key': '161245ASDF', 69 'vserver': 'vserver', 70 } 71 return args 72 73 74# REST API canned responses when mocking send_request 75SRR = { 76 # common responses 77 'is_rest': (200, dict(version=dict(generation=9, major=9, minor=0, full='dummy')), None), 78 'is_rest_9_6': (200, dict(version=dict(generation=9, major=6, minor=0, full='dummy')), None), 79 'is_rest_9_8': (200, dict(version=dict(generation=9, major=8, minor=0, full='dummy')), None), 80 'is_zapi': (400, {}, "Unreachable"), 81 'empty_good': (200, {}, None), 82 'zero_record': (200, dict(records=[], num_records=0), None), 83 'one_record_uuid': (200, dict(records=[dict(uuid='a1b2c3')], num_records=1), None), 84 'end_of_sequence': (500, None, "Unexpected call to send_request"), 85 'generic_error': (400, None, "Expected error"), 86 'one_pk_record': (200, { 87 "records": [{ 88 'account': dict(name='user123'), 89 'owner': dict(uuid='98765'), 90 'public_key': '161245ASDF', 91 'index': 12, 92 'comment': 'comment_123', 93 }], 94 'num_records': 1 95 }, None), 96 'two_pk_records': (200, { 97 "records": [{ 98 'account': dict(name='user123'), 99 'owner': dict(uuid='98765'), 100 'public_key': '161245ASDF', 101 'index': 12, 102 'comment': 'comment_123', 103 }, 104 { 105 'account': dict(name='user123'), 106 'owner': dict(uuid='98765'), 107 'public_key': '161245ASDF', 108 'index': 13, 109 'comment': 'comment_123', 110 }], 111 'num_records': 2 112 }, None) 113} 114 115 116# using pytest natively, without unittest.TestCase 117@pytest.fixture 118def patch_ansible(): 119 with patch.multiple(basic.AnsibleModule, 120 exit_json=exit_json, 121 fail_json=fail_json, 122 warn=warn) as mocks: 123 global WARNINGS 124 WARNINGS = list() 125 yield mocks 126 127 128def test_module_fail_when_required_args_missing(patch_ansible): 129 ''' required arguments are reported as errors ''' 130 with pytest.raises(AnsibleFailJson) as exc: 131 set_module_args(dict(hostname='')) 132 my_module() 133 print('Info: %s' % exc.value.args[0]['msg']) 134 msg = 'missing required arguments: account' 135 assert msg == exc.value.args[0]['msg'] 136 137 138@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') 139def test_ensure_get_called(mock_request, patch_ansible): 140 ''' test get''' 141 args = dict(default_args()) 142 args['index'] = 12 143 set_module_args(args) 144 mock_request.side_effect = [ 145 SRR['is_rest_9_8'], # get version 146 SRR['one_pk_record'], # get 147 SRR['end_of_sequence'] 148 ] 149 my_obj = my_module() 150 with pytest.raises(AnsibleExitJson) as exc: 151 my_obj.apply() 152 print('Info: %s' % exc.value.args[0]) 153 assert exc.value.args[0]['changed'] is False 154 assert not WARNINGS 155 156 157@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') 158def test_ensure_create_called(mock_request, patch_ansible): 159 ''' test get''' 160 args = dict(default_args()) 161 args['use_rest'] = 'auto' 162 args['index'] = 13 163 set_module_args(args) 164 mock_request.side_effect = [ 165 SRR['is_rest_9_8'], # get version 166 SRR['zero_record'], # get 167 SRR['empty_good'], # create 168 SRR['end_of_sequence'] 169 ] 170 my_obj = my_module() 171 with pytest.raises(AnsibleExitJson) as exc: 172 my_obj.apply() 173 print('Info: %s' % exc.value.args[0]) 174 assert exc.value.args[0]['changed'] is True 175 assert not WARNINGS 176 177 178@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') 179def test_ensure_create_idempotent(mock_request, patch_ansible): 180 ''' test get''' 181 args = dict(default_args()) 182 args['use_rest'] = 'always' 183 args['index'] = 12 184 set_module_args(args) 185 mock_request.side_effect = [ 186 SRR['is_rest_9_8'], # get version 187 SRR['one_pk_record'], # get 188 SRR['end_of_sequence'] 189 ] 190 my_obj = my_module() 191 with pytest.raises(AnsibleExitJson) as exc: 192 my_obj.apply() 193 print('Info: %s' % exc.value.args[0]) 194 assert exc.value.args[0]['changed'] is False 195 assert not WARNINGS 196 197 198@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') 199def test_ensure_create_always_called(mock_request, patch_ansible): 200 ''' test get''' 201 args = dict(default_args()) 202 set_module_args(args) 203 mock_request.side_effect = [ 204 SRR['is_rest_9_8'], # get version 205 SRR['empty_good'], # create 206 SRR['end_of_sequence'] 207 ] 208 my_obj = my_module() 209 with pytest.raises(AnsibleExitJson) as exc: 210 my_obj.apply() 211 print('Info: %s' % exc.value.args[0]) 212 assert exc.value.args[0]['changed'] is True 213 print(WARNINGS) 214 assert 'Module is not idempotent if index is not provided with state=present.' in WARNINGS 215 216 217@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') 218def test_ensure_modify_called(mock_request, patch_ansible): 219 ''' test get''' 220 args = dict(default_args()) 221 args['index'] = 12 222 args['comment'] = 'new_comment' 223 set_module_args(args) 224 mock_request.side_effect = [ 225 SRR['is_rest_9_8'], # get version 226 SRR['one_pk_record'], # get 227 SRR['empty_good'], # modify 228 SRR['end_of_sequence'] 229 ] 230 my_obj = my_module() 231 with pytest.raises(AnsibleExitJson) as exc: 232 my_obj.apply() 233 print('Info: %s' % exc.value.args[0]) 234 assert exc.value.args[0]['changed'] is True 235 assert not WARNINGS 236 237 238@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') 239def test_ensure_delete_called(mock_request, patch_ansible): 240 ''' test get''' 241 args = dict(default_args()) 242 args['use_rest'] = 'auto' 243 args['index'] = 12 244 args['state'] = 'absent' 245 set_module_args(args) 246 mock_request.side_effect = [ 247 SRR['is_rest_9_8'], # get version 248 SRR['one_pk_record'], # get 249 SRR['empty_good'], # delete 250 SRR['end_of_sequence'] 251 ] 252 my_obj = my_module() 253 with pytest.raises(AnsibleExitJson) as exc: 254 my_obj.apply() 255 print('Info: %s' % exc.value.args[0]) 256 assert exc.value.args[0]['changed'] is True 257 assert not WARNINGS 258 259 260@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') 261def test_ensure_delete_idempotent(mock_request, patch_ansible): 262 ''' test get''' 263 args = dict(default_args()) 264 args['use_rest'] = 'always' 265 args['index'] = 12 266 args['state'] = 'absent' 267 set_module_args(args) 268 mock_request.side_effect = [ 269 SRR['is_rest_9_8'], # get version 270 SRR['zero_record'], # get 271 SRR['end_of_sequence'] 272 ] 273 my_obj = my_module() 274 with pytest.raises(AnsibleExitJson) as exc: 275 my_obj.apply() 276 print('Info: %s' % exc.value.args[0]) 277 assert exc.value.args[0]['changed'] is False 278 assert not WARNINGS 279 280 281@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') 282def test_ensure_delete_failed_N_records(mock_request, patch_ansible): 283 ''' test get''' 284 args = dict(default_args()) 285 args['use_rest'] = 'auto' 286 args['state'] = 'absent' 287 set_module_args(args) 288 mock_request.side_effect = [ 289 SRR['is_rest_9_8'], # get version 290 SRR['two_pk_records'], # get 291 SRR['end_of_sequence'] 292 ] 293 my_obj = my_module() 294 with pytest.raises(AnsibleFailJson) as exc: 295 my_obj.apply() 296 print('Info: %s' % exc.value.args[0]) 297 msg = 'Error: index is required as more than one public_key exists for user account user123' 298 assert msg in exc.value.args[0]['msg'] 299 assert not WARNINGS 300 301 302@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') 303def test_ensure_delete_succeeded_N_records(mock_request, patch_ansible): 304 ''' test get''' 305 args = dict(default_args()) 306 args['use_rest'] = 'auto' 307 args['state'] = 'absent' 308 args['delete_all'] = True 309 set_module_args(args) 310 mock_request.side_effect = [ 311 SRR['is_rest_9_8'], # get version 312 SRR['two_pk_records'], # get 313 SRR['empty_good'], # delete 314 SRR['empty_good'], # delete 315 SRR['end_of_sequence'] 316 ] 317 my_obj = my_module() 318 with pytest.raises(AnsibleExitJson) as exc: 319 my_obj.apply() 320 print('Info: %s' % exc.value.args[0]) 321 assert exc.value.args[0]['changed'] is True 322 assert not WARNINGS 323 324 325@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') 326def test_ensure_delete_succeeded_N_records_cluster(mock_request, patch_ansible): 327 ''' test get''' 328 args = dict(default_args()) 329 args['use_rest'] = 'auto' 330 args['state'] = 'absent' 331 args['delete_all'] = True 332 args['vserver'] = None # cluster scope 333 set_module_args(args) 334 mock_request.side_effect = [ 335 SRR['is_rest_9_8'], # get version 336 SRR['two_pk_records'], # get 337 SRR['empty_good'], # delete 338 SRR['empty_good'], # delete 339 SRR['end_of_sequence'] 340 ] 341 with pytest.raises(AnsibleExitJson) as exc: 342 uut_main() 343 print('Info: %s' % exc.value.args[0]) 344 assert exc.value.args[0]['changed'] is True 345 assert not WARNINGS 346 347 348@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') 349def test_negative_extra_record(mock_request, patch_ansible): 350 ''' test get''' 351 args = dict(default_args()) 352 args['use_rest'] = 'auto' 353 args['state'] = 'present' 354 args['index'] = 14 355 args['vserver'] = None # cluster scope 356 set_module_args(args) 357 mock_request.side_effect = [ 358 SRR['is_rest_9_8'], # get version 359 SRR['two_pk_records'], # get 360 SRR['end_of_sequence'] 361 ] 362 with pytest.raises(AnsibleFailJson) as exc: 363 uut_main() 364 print('Info: %s' % exc.value.args[0]) 365 msg = 'Error in get_public_key: calling: security/authentication/publickeys: unexpected response' 366 assert msg in exc.value.args[0]['msg'] 367 assert not WARNINGS 368 369 370@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') 371def test_negative_extra_arg_in_modify(mock_request, patch_ansible): 372 ''' test get''' 373 args = dict(default_args()) 374 args['use_rest'] = 'auto' 375 args['state'] = 'present' 376 args['index'] = 14 377 args['vserver'] = None # cluster scope 378 set_module_args(args) 379 mock_request.side_effect = [ 380 SRR['is_rest_9_8'], # get version 381 SRR['one_pk_record'], # get 382 SRR['end_of_sequence'] 383 ] 384 with pytest.raises(AnsibleFailJson) as exc: 385 uut_main() 386 print('Info: %s' % exc.value.args[0]) 387 msg = "Error: attributes not supported in modify: {'index': 14}" 388 assert msg in exc.value.args[0]['msg'] 389 assert not WARNINGS 390 391 392@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') 393def test_negative_empty_body_in_modify(mock_request, patch_ansible): 394 ''' test get''' 395 args = dict(default_args()) 396 set_module_args(args) 397 mock_request.side_effect = [ 398 SRR['is_rest_9_8'], # get version 399 SRR['end_of_sequence'] 400 ] 401 current = dict(owner=dict(uuid=''), account=dict(name=''), index=0) 402 modify = dict() 403 my_obj = my_module() 404 with pytest.raises(AnsibleFailJson) as exc: 405 my_obj.modify_public_key(current, modify) 406 print('Info: %s' % exc.value.args[0]) 407 msg = 'Error: nothing to change - modify called with: {}' 408 assert msg in exc.value.args[0]['msg'] 409 assert not WARNINGS 410 411 412@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') 413def test_negative_create_called(mock_request, patch_ansible): 414 ''' test get''' 415 args = dict(default_args()) 416 args['use_rest'] = 'auto' 417 args['index'] = 13 418 set_module_args(args) 419 mock_request.side_effect = [ 420 SRR['is_rest_9_8'], # get version 421 SRR['zero_record'], # get 422 SRR['generic_error'], # create 423 SRR['end_of_sequence'] 424 ] 425 my_obj = my_module() 426 with pytest.raises(AnsibleFailJson) as exc: 427 my_obj.apply() 428 print('Info: %s' % exc.value.args[0]) 429 msg = 'Error in create_public_key: Expected error' 430 assert msg in exc.value.args[0]['msg'] 431 assert not WARNINGS 432 433 434@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') 435def test_negative_delete_called(mock_request, patch_ansible): 436 ''' test get''' 437 args = dict(default_args()) 438 args['use_rest'] = 'auto' 439 args['index'] = 12 440 args['state'] = 'absent' 441 set_module_args(args) 442 mock_request.side_effect = [ 443 SRR['is_rest_9_8'], # get version 444 SRR['one_pk_record'], # get 445 SRR['generic_error'], # delete 446 SRR['end_of_sequence'] 447 ] 448 my_obj = my_module() 449 with pytest.raises(AnsibleFailJson) as exc: 450 my_obj.apply() 451 print('Info: %s' % exc.value.args[0]) 452 msg = 'Error in delete_public_key: Expected error' 453 assert msg in exc.value.args[0]['msg'] 454 assert not WARNINGS 455 456 457@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') 458def test_negative_modify_called(mock_request, patch_ansible): 459 ''' test get''' 460 args = dict(default_args()) 461 args['use_rest'] = 'auto' 462 args['index'] = 12 463 args['comment'] = 'change_me' 464 set_module_args(args) 465 mock_request.side_effect = [ 466 SRR['is_rest_9_8'], # get version 467 SRR['one_pk_record'], # get 468 SRR['generic_error'], # modify 469 SRR['end_of_sequence'] 470 ] 471 my_obj = my_module() 472 with pytest.raises(AnsibleFailJson) as exc: 473 my_obj.apply() 474 print('Info: %s' % exc.value.args[0]) 475 msg = 'Error in modify_public_key: Expected error' 476 assert msg in exc.value.args[0]['msg'] 477 assert not WARNINGS 478 479 480@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') 481def test_negative_older_version(mock_request, patch_ansible): 482 ''' test get''' 483 args = dict(default_args()) 484 args['use_rest'] = 'auto' 485 args['index'] = 12 486 args['comment'] = 'change_me' 487 set_module_args(args) 488 mock_request.side_effect = [ 489 SRR['is_rest_9_6'], # get version 490 SRR['end_of_sequence'] 491 ] 492 with pytest.raises(AnsibleFailJson) as exc: 493 my_obj = my_module() 494 print('Info: %s' % exc.value.args[0]) 495 msg = 'Error: na_ontap_publickey only supports REST, and requires ONTAP 9.7 or later. Found: 9.6.' 496 assert msg in exc.value.args[0]['msg'] 497 assert not WARNINGS 498 499 500@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request') 501def test_negative_zapi_only(mock_request, patch_ansible): 502 ''' test get''' 503 args = dict(default_args()) 504 args['use_rest'] = 'never' 505 args['index'] = 12 506 args['comment'] = 'change_me' 507 set_module_args(args) 508 mock_request.side_effect = [ 509 SRR['is_rest_9_6'], # get version 510 SRR['end_of_sequence'] 511 ] 512 with pytest.raises(AnsibleFailJson) as exc: 513 my_obj = my_module() 514 print('Info: %s' % exc.value.args[0]) 515 msg = 'Error: REST is required for this module, found: "use_rest: never"' 516 assert msg in exc.value.args[0]['msg'] 517 assert not WARNINGS 518