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