1# (c) 2019, NetApp, Inc
2# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
3
4""" unit tests for Ansible module: na_ontap_security_certificates """
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_security_certificates \
18    import NetAppOntapSecurityCertificates as my_module  # 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
25# REST API canned responses when mocking send_request
26SRR = {
27    # common responses
28    'is_rest': (200, {}, None),
29    'is_zapi': (400, {}, "Unreachable"),
30    'empty_good': (200, {}, None),
31    'end_of_sequence': (500, None, "Unexpected call to send_request"),
32    'generic_error': (400, None, "Expected error"),
33    # module specific responses
34    'empty_records': (200, {'records': []}, None),
35    'get_uuid': (200, {'records': [{'uuid': 'ansible'}]}, None),
36    'error_unexpected_name': (200, None, {'message': 'Unexpected argument "name".'})
37}
38
39NAME_ERROR = "Error calling API: security/certificates - ONTAP 9.6 and 9.7 do not support 'name'.  Use 'common_name' and 'type' as a work-around."
40TYPE_ERROR = "Error calling API: security/certificates - When using 'common_name', 'type' is required."
41EXPECTED_ERROR = "Error calling API: security/certificates - Expected error"
42
43
44def set_module_args(args):
45    """prepare arguments so that they will be picked up during module creation"""
46    args = json.dumps({'ANSIBLE_MODULE_ARGS': args})
47    basic._ANSIBLE_ARGS = to_bytes(args)  # pylint: disable=protected-access
48
49
50class AnsibleExitJson(Exception):
51    """Exception class to be raised by module.exit_json and caught by the test case"""
52
53
54class AnsibleFailJson(Exception):
55    """Exception class to be raised by module.fail_json and caught by the test case"""
56
57
58def exit_json(*args, **kwargs):  # pylint: disable=unused-argument
59    """function to patch over exit_json; package return data into an exception"""
60    if 'changed' not in kwargs:
61        kwargs['changed'] = False
62    raise AnsibleExitJson(kwargs)
63
64
65def fail_json(*args, **kwargs):  # pylint: disable=unused-argument
66    """function to patch over fail_json; package return data into an exception"""
67    kwargs['failed'] = True
68    raise AnsibleFailJson(kwargs)
69
70
71def set_default_args():
72    return dict({
73        'hostname': 'hostname',
74        'username': 'username',
75        'password': 'password',
76        'name': 'name_for_certificate'
77    })
78
79
80@patch('ansible.module_utils.basic.AnsibleModule.fail_json')
81def test_module_fail_when_required_args_missing(mock_fail):
82    ''' required arguments are reported as errors '''
83    mock_fail.side_effect = fail_json
84    set_module_args({})
85    with pytest.raises(AnsibleFailJson) as exc:
86        my_module()
87    print('Info: %s' % exc.value.args[0]['msg'])
88
89
90@patch('ansible.module_utils.basic.AnsibleModule.fail_json')
91@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request')
92def test_ensure_get_certificate_called(mock_request, mock_fail):
93    mock_fail.side_effect = fail_json
94    mock_request.side_effect = [
95        SRR['is_rest'],
96        SRR['get_uuid'],
97        SRR['end_of_sequence']
98    ]
99    set_module_args(set_default_args())
100    my_obj = my_module()
101    assert my_obj.get_certificate() is not None
102
103
104@patch('ansible.module_utils.basic.AnsibleModule.fail_json')
105@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request')
106def test_rest_error(mock_request, mock_fail):
107    mock_fail.side_effect = fail_json
108    mock_request.side_effect = [
109        SRR['is_rest'],
110        SRR['generic_error'],
111        SRR['end_of_sequence']
112    ]
113    set_module_args(set_default_args())
114    my_obj = my_module()
115    with pytest.raises(AnsibleFailJson) as exc:
116        my_obj.apply()
117    assert exc.value.args[0]['msg'] == EXPECTED_ERROR
118
119
120@patch('ansible.module_utils.basic.AnsibleModule.exit_json')
121@patch('ansible.module_utils.basic.AnsibleModule.fail_json')
122@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request')
123def test_rest_create_failed(mock_request, mock_fail, mock_exit):
124    mock_exit.side_effect = exit_json
125    mock_fail.side_effect = fail_json
126    mock_request.side_effect = [
127        SRR['is_rest'],
128        SRR['empty_records'],   # get certificate -> not found
129        SRR['empty_good'],
130        SRR['end_of_sequence']
131    ]
132    data = {
133        'type': 'client_ca',
134        'vserver': 'abc',
135    }
136    data.update(set_default_args())
137    set_module_args(data)
138    my_obj = my_module()
139    with pytest.raises(AnsibleFailJson) as exc:
140        my_obj.apply()
141    msg = 'Error creating or installing certificate: one or more of the following options are missing:'
142    assert exc.value.args[0]['msg'].startswith(msg)
143
144
145@patch('ansible.module_utils.basic.AnsibleModule.exit_json')
146@patch('ansible.module_utils.basic.AnsibleModule.fail_json')
147@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request')
148def test_rest_successful_create(mock_request, mock_fail, mock_exit):
149    mock_exit.side_effect = exit_json
150    mock_fail.side_effect = fail_json
151    mock_request.side_effect = [
152        SRR['is_rest'],
153        SRR['empty_records'],   # get certificate -> not found
154        SRR['empty_good'],
155        SRR['end_of_sequence']
156    ]
157    data = {
158        'type': 'client_ca',
159        'vserver': 'abc',
160        'common_name': 'cname'
161    }
162    data.update(set_default_args())
163    set_module_args(data)
164    my_obj = my_module()
165    with pytest.raises(AnsibleExitJson) as exc:
166        my_obj.apply()
167    assert exc.value.args[0]['changed']
168
169
170@patch('ansible.module_utils.basic.AnsibleModule.exit_json')
171@patch('ansible.module_utils.basic.AnsibleModule.fail_json')
172@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request')
173def test_rest_idempotent_create(mock_request, mock_fail, mock_exit):
174    mock_exit.side_effect = exit_json
175    mock_fail.side_effect = fail_json
176    mock_request.side_effect = [
177        SRR['is_rest'],
178        SRR['get_uuid'],    # get certificate -> found
179        SRR['end_of_sequence']
180    ]
181    data = {
182        'type': 'client_ca',
183        'vserver': 'abc',
184    }
185    data.update(set_default_args())
186    set_module_args(data)
187    my_obj = my_module()
188    with pytest.raises(AnsibleExitJson) as exc:
189        my_obj.apply()
190    assert not exc.value.args[0]['changed']
191
192
193@patch('ansible.module_utils.basic.AnsibleModule.exit_json')
194@patch('ansible.module_utils.basic.AnsibleModule.fail_json')
195@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request')
196def test_rest_successful_delete(mock_request, mock_fail, mock_exit):
197    mock_exit.side_effect = exit_json
198    mock_fail.side_effect = fail_json
199    mock_request.side_effect = [
200        SRR['is_rest'],
201        SRR['get_uuid'],    # get certificate -> found
202        SRR['empty_good'],
203        SRR['end_of_sequence']
204    ]
205    data = {
206        'state': 'absent',
207    }
208    data.update(set_default_args())
209    set_module_args(data)
210    my_obj = my_module()
211    with pytest.raises(AnsibleExitJson) as exc:
212        my_obj.apply()
213    assert exc.value.args[0]['changed']
214
215
216@patch('ansible.module_utils.basic.AnsibleModule.exit_json')
217@patch('ansible.module_utils.basic.AnsibleModule.fail_json')
218@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request')
219def test_rest_idempotent_delete(mock_request, mock_fail, mock_exit):
220    mock_exit.side_effect = exit_json
221    mock_fail.side_effect = fail_json
222    mock_request.side_effect = [
223        SRR['is_rest'],
224        SRR['empty_records'],   # get certificate -> not found
225        SRR['empty_good'],
226        SRR['end_of_sequence']
227    ]
228    data = {
229        'state': 'absent',
230    }
231    data.update(set_default_args())
232    set_module_args(data)
233    my_obj = my_module()
234    with pytest.raises(AnsibleExitJson) as exc:
235        my_obj.apply()
236    assert not exc.value.args[0]['changed']
237
238
239@patch('ansible.module_utils.basic.AnsibleModule.exit_json')
240@patch('ansible.module_utils.basic.AnsibleModule.fail_json')
241@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request')
242def test_rest_successful_sign(mock_request, mock_fail, mock_exit):
243    mock_exit.side_effect = exit_json
244    mock_fail.side_effect = fail_json
245    mock_request.side_effect = [
246        SRR['is_rest'],
247        SRR['get_uuid'],    # get certificate -> found
248        SRR['empty_good'],
249        SRR['end_of_sequence']
250    ]
251    data = {
252        'vserver': 'abc',
253        'signing_request': 'CSR'
254    }
255    data.update(set_default_args())
256    set_module_args(data)
257    my_obj = my_module()
258    with pytest.raises(AnsibleExitJson) as exc:
259        my_obj.apply()
260    assert exc.value.args[0]['changed']
261
262
263@patch('ansible.module_utils.basic.AnsibleModule.exit_json')
264@patch('ansible.module_utils.basic.AnsibleModule.fail_json')
265@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request')
266def test_rest_failed_sign_missing_ca(mock_request, mock_fail, mock_exit):
267    mock_exit.side_effect = exit_json
268    mock_fail.side_effect = fail_json
269    mock_request.side_effect = [
270        SRR['is_rest'],
271        SRR['empty_records'],   # get certificate -> not found
272        SRR['empty_good'],
273        SRR['end_of_sequence']
274    ]
275    data = {
276        'vserver': 'abc',
277        'signing_request': 'CSR'
278    }
279    data.update(set_default_args())
280    set_module_args(data)
281    my_obj = my_module()
282    with pytest.raises(AnsibleFailJson) as exc:
283        my_obj.apply()
284    msg = "signing certificate with name '%s' not found on svm: %s" % (data['name'], data['vserver'])
285    assert exc.value.args[0]['msg'] == msg
286
287
288@patch('ansible.module_utils.basic.AnsibleModule.exit_json')
289@patch('ansible.module_utils.basic.AnsibleModule.fail_json')
290@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request')
291def test_rest_failed_sign_absent(mock_request, mock_fail, mock_exit):
292    mock_exit.side_effect = exit_json
293    mock_fail.side_effect = fail_json
294    mock_request.side_effect = [
295        SRR['is_rest'],
296        SRR['get_uuid'],    # get certificate -> found
297        SRR['end_of_sequence']
298    ]
299    data = {
300        'vserver': 'abc',
301        'signing_request': 'CSR',
302        'state': 'absent'
303    }
304    data.update(set_default_args())
305    set_module_args(data)
306    my_obj = my_module()
307    with pytest.raises(AnsibleFailJson) as exc:
308        my_obj.apply()
309    msg = "'signing_request' is not supported with 'state' set to 'absent'"
310    assert exc.value.args[0]['msg'] == msg
311
312
313@patch('ansible.module_utils.basic.AnsibleModule.exit_json')
314@patch('ansible.module_utils.basic.AnsibleModule.fail_json')
315@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request')
316def test_rest_failed_on_name(mock_request, mock_fail, mock_exit):
317    mock_exit.side_effect = exit_json
318    mock_fail.side_effect = fail_json
319    mock_request.side_effect = [
320        SRR['is_rest'],
321        SRR['error_unexpected_name'],   # get certificate -> error
322        SRR['end_of_sequence']
323    ]
324    data = {
325        'vserver': 'abc',
326        'signing_request': 'CSR',
327        'state': 'absent',
328        'ignore_name_if_not_supported': False,
329        'common_name': 'common_name',
330        'type': 'root_ca'
331    }
332    data.update(set_default_args())
333    set_module_args(data)
334    my_obj = my_module()
335    with pytest.raises(AnsibleFailJson) as exc:
336        my_obj.apply()
337    assert exc.value.args[0]['msg'] == NAME_ERROR
338
339
340@patch('ansible.module_utils.basic.AnsibleModule.exit_json')
341@patch('ansible.module_utils.basic.AnsibleModule.fail_json')
342@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request')
343def test_rest_cannot_ignore_name_error_no_common_name(mock_request, mock_fail, mock_exit):
344    mock_exit.side_effect = exit_json
345    mock_fail.side_effect = fail_json
346    mock_request.side_effect = [
347        SRR['is_rest'],
348        SRR['error_unexpected_name'],   # get certificate -> error
349        SRR['end_of_sequence']
350    ]
351    data = {
352        'vserver': 'abc',
353        'signing_request': 'CSR',
354        'state': 'absent',
355    }
356    data.update(set_default_args())
357    set_module_args(data)
358    my_obj = my_module()
359    with pytest.raises(AnsibleFailJson) as exc:
360        my_obj.apply()
361    assert exc.value.args[0]['msg'] == NAME_ERROR
362
363
364@patch('ansible.module_utils.basic.AnsibleModule.exit_json')
365@patch('ansible.module_utils.basic.AnsibleModule.fail_json')
366@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request')
367def test_rest_cannot_ignore_name_error_no_type(mock_request, mock_fail, mock_exit):
368    mock_exit.side_effect = exit_json
369    mock_fail.side_effect = fail_json
370    mock_request.side_effect = [
371        SRR['is_rest'],
372        SRR['error_unexpected_name'],   # get certificate -> error
373        SRR['end_of_sequence']
374    ]
375    data = {
376        'vserver': 'abc',
377        'signing_request': 'CSR',
378        'state': 'absent',
379        'common_name': 'common_name'
380    }
381    data.update(set_default_args())
382    set_module_args(data)
383    my_obj = my_module()
384    with pytest.raises(AnsibleFailJson) as exc:
385        my_obj.apply()
386    assert exc.value.args[0]['msg'] == TYPE_ERROR
387
388
389@patch('ansible.module_utils.basic.AnsibleModule.exit_json')
390@patch('ansible.module_utils.basic.AnsibleModule.fail_json')
391@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request')
392def test_rest_ignore_name_error(mock_request, mock_fail, mock_exit):
393    mock_exit.side_effect = exit_json
394    mock_fail.side_effect = fail_json
395    mock_request.side_effect = [
396        SRR['is_rest'],
397        SRR['error_unexpected_name'],   # get certificate -> error
398        SRR['get_uuid'],                # get certificate -> found
399        SRR['end_of_sequence']
400    ]
401    data = {
402        'vserver': 'abc',
403        'signing_request': 'CSR',
404        'state': 'absent',
405        'common_name': 'common_name',
406        'type': 'root_ca'
407    }
408    data.update(set_default_args())
409    set_module_args(data)
410    my_obj = my_module()
411    with pytest.raises(AnsibleFailJson) as exc:
412        my_obj.apply()
413    msg = "'signing_request' is not supported with 'state' set to 'absent'"
414    assert exc.value.args[0]['msg'] == msg
415
416
417@patch('ansible.module_utils.basic.AnsibleModule.exit_json')
418@patch('ansible.module_utils.basic.AnsibleModule.fail_json')
419@patch('ansible_collections.netapp.ontap.plugins.module_utils.netapp.OntapRestAPI.send_request')
420def test_rest_successful_create_name_error(mock_request, mock_fail, mock_exit):
421    mock_exit.side_effect = exit_json
422    mock_fail.side_effect = fail_json
423    mock_request.side_effect = [
424        SRR['is_rest'],
425        SRR['error_unexpected_name'],   # get certificate -> error
426        SRR['empty_records'],           # get certificate -> not found
427        SRR['empty_good'],
428        SRR['end_of_sequence']
429    ]
430    data = {
431        'common_name': 'cname',
432        'type': 'client_ca',
433        'vserver': 'abc',
434    }
435    data.update(set_default_args())
436    set_module_args(data)
437    my_obj = my_module()
438    with pytest.raises(AnsibleExitJson) as exc:
439        my_obj.apply()
440    assert exc.value.args[0]['changed']
441    print(mock_request.mock_calls)
442