1# (c) 2018, 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 template for ONTAP Ansible module '''
5
6from __future__ import print_function
7import json
8import pytest
9
10from units.compat import unittest
11from units.compat.mock import patch, Mock
12from ansible.module_utils import basic
13from ansible.module_utils._text import to_bytes
14import ansible.module_utils.netapp as netapp_utils
15
16from ansible.modules.storage.netapp.na_ontap_unix_user \
17    import NetAppOntapUnixUser as user_module  # module under test
18
19if not netapp_utils.has_netapp_lib():
20    pytestmark = pytest.mark.skip('skipping as missing required netapp_lib')
21
22
23def set_module_args(args):
24    """prepare arguments so that they will be picked up during module creation"""
25    args = json.dumps({'ANSIBLE_MODULE_ARGS': args})
26    basic._ANSIBLE_ARGS = to_bytes(args)  # pylint: disable=protected-access
27
28
29class AnsibleExitJson(Exception):
30    """Exception class to be raised by module.exit_json and caught by the test case"""
31    pass
32
33
34class AnsibleFailJson(Exception):
35    """Exception class to be raised by module.fail_json and caught by the test case"""
36    pass
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
52class MockONTAPConnection(object):
53    ''' mock server connection to ONTAP host '''
54
55    def __init__(self, kind=None, data=None):
56        ''' save arguments '''
57        self.kind = kind
58        self.params = data
59        self.xml_in = None
60        self.xml_out = None
61
62    def invoke_successfully(self, xml, enable_tunneling):  # pylint: disable=unused-argument
63        ''' mock invoke_successfully returning xml data '''
64        self.xml_in = xml
65        if self.kind == 'user':
66            xml = self.build_user_info(self.params)
67        elif self.kind == 'user-fail':
68            raise netapp_utils.zapi.NaApiError(code='TEST', message="This exception is from the unit test")
69        self.xml_out = xml
70        return xml
71
72    @staticmethod
73    def build_user_info(data):
74        ''' build xml data for vserser-info '''
75        xml = netapp_utils.zapi.NaElement('xml')
76        attributes = \
77            {'attributes-list': {'unix-user-info': {'user-id': data['id'],
78                                                    'group-id': data['group_id'], 'full-name': data['full_name']}},
79             'num-records': 1}
80        xml.translate_struct(attributes)
81        return xml
82
83
84class TestMyModule(unittest.TestCase):
85    ''' a group of related Unit Tests '''
86
87    def setUp(self):
88        self.mock_module_helper = patch.multiple(basic.AnsibleModule,
89                                                 exit_json=exit_json,
90                                                 fail_json=fail_json)
91        self.mock_module_helper.start()
92        self.addCleanup(self.mock_module_helper.stop)
93        self.server = MockONTAPConnection()
94        self.mock_user = {
95            'name': 'test',
96            'id': '11',
97            'group_id': '12',
98            'vserver': 'something',
99            'full_name': 'Test User'
100        }
101
102    def mock_args(self):
103        return {
104            'name': self.mock_user['name'],
105            'group_id': self.mock_user['group_id'],
106            'id': self.mock_user['id'],
107            'vserver': self.mock_user['vserver'],
108            'full_name': self.mock_user['full_name'],
109            'hostname': 'test',
110            'username': 'test_user',
111            'password': 'test_pass!'
112        }
113
114    def get_user_mock_object(self, kind=None, data=None):
115        """
116        Helper method to return an na_ontap_unix_user object
117        :param kind: passes this param to MockONTAPConnection()
118        :return: na_ontap_unix_user object
119        """
120        obj = user_module()
121        obj.autosupport_log = Mock(return_value=None)
122        if data is None:
123            data = self.mock_user
124        obj.server = MockONTAPConnection(kind=kind, data=data)
125        return obj
126
127    def test_module_fail_when_required_args_missing(self):
128        ''' required arguments are reported as errors '''
129        with pytest.raises(AnsibleFailJson) as exc:
130            set_module_args({})
131            user_module()
132
133    def test_get_nonexistent_user(self):
134        ''' Test if get_unix_user returns None for non-existent user '''
135        set_module_args(self.mock_args())
136        result = self.get_user_mock_object().get_unix_user()
137        assert result is None
138
139    def test_get_existing_user(self):
140        ''' Test if get_unix_user returns details for existing user '''
141        set_module_args(self.mock_args())
142        result = self.get_user_mock_object('user').get_unix_user()
143        assert result['full_name'] == self.mock_user['full_name']
144
145    def test_get_xml(self):
146        set_module_args(self.mock_args())
147        obj = self.get_user_mock_object('user')
148        result = obj.get_unix_user()
149        assert obj.server.xml_in['query']
150        assert obj.server.xml_in['query']['unix-user-info']
151        user_info = obj.server.xml_in['query']['unix-user-info']
152        assert user_info['user-name'] == self.mock_user['name']
153        assert user_info['vserver'] == self.mock_user['vserver']
154
155    def test_create_error_missing_params(self):
156        data = self.mock_args()
157        del data['group_id']
158        set_module_args(data)
159        with pytest.raises(AnsibleFailJson) as exc:
160            self.get_user_mock_object('user').create_unix_user()
161        assert 'Error: Missing one or more required parameters for create: (group_id, id)' == exc.value.args[0]['msg']
162
163    @patch('ansible.modules.storage.netapp.na_ontap_unix_user.NetAppOntapUnixUser.create_unix_user')
164    def test_create_called(self, create_user):
165        set_module_args(self.mock_args())
166        with pytest.raises(AnsibleExitJson) as exc:
167            self.get_user_mock_object().apply()
168        assert exc.value.args[0]['changed']
169        create_user.assert_called_with()
170
171    def test_create_xml(self):
172        '''Test create ZAPI element'''
173        set_module_args(self.mock_args())
174        create = self.get_user_mock_object()
175        with pytest.raises(AnsibleExitJson) as exc:
176            create.apply()
177        mock_key = {
178            'user-name': 'name',
179            'group-id': 'group_id',
180            'user-id': 'id',
181            'full-name': 'full_name'
182        }
183        for key in ['user-name', 'user-id', 'group-id', 'full-name']:
184            assert create.server.xml_in[key] == self.mock_user[mock_key[key]]
185
186    def test_create_wihtout_full_name(self):
187        '''Test create ZAPI element'''
188        data = self.mock_args()
189        del data['full_name']
190        set_module_args(data)
191        create = self.get_user_mock_object()
192        with pytest.raises(AnsibleExitJson) as exc:
193            create.apply()
194        with pytest.raises(KeyError):
195            create.server.xml_in['full-name']
196
197    @patch('ansible.modules.storage.netapp.na_ontap_unix_user.NetAppOntapUnixUser.modify_unix_user')
198    @patch('ansible.modules.storage.netapp.na_ontap_unix_user.NetAppOntapUnixUser.delete_unix_user')
199    def test_delete_called(self, delete_user, modify_user):
200        ''' Test delete existing user '''
201        data = self.mock_args()
202        data['state'] = 'absent'
203        set_module_args(data)
204        with pytest.raises(AnsibleExitJson) as exc:
205            self.get_user_mock_object('user').apply()
206        assert exc.value.args[0]['changed']
207        delete_user.assert_called_with()
208        assert modify_user.call_count == 0
209
210    @patch('ansible.modules.storage.netapp.na_ontap_unix_user.NetAppOntapUnixUser.get_unix_user')
211    @patch('ansible.modules.storage.netapp.na_ontap_unix_user.NetAppOntapUnixUser.modify_unix_user')
212    def test_modify_called(self, modify_user, get_user):
213        ''' Test modify user group_id '''
214        data = self.mock_args()
215        data['group_id'] = 20
216        set_module_args(data)
217        get_user.return_value = {'group_id': 10}
218        obj = self.get_user_mock_object('user')
219        with pytest.raises(AnsibleExitJson) as exc:
220            obj.apply()
221        get_user.assert_called_with()
222        modify_user.assert_called_with({'group_id': 20})
223
224    def test_modify_only_id(self):
225        ''' Test modify user id '''
226        set_module_args(self.mock_args())
227        modify = self.get_user_mock_object('user')
228        modify.modify_unix_user({'id': 123})
229        assert modify.server.xml_in['user-id'] == '123'
230        with pytest.raises(KeyError):
231            modify.server.xml_in['group-id']
232        with pytest.raises(KeyError):
233            modify.server.xml_in['full-name']
234
235    def test_modify_xml(self):
236        ''' Test modify user full_name '''
237        set_module_args(self.mock_args())
238        modify = self.get_user_mock_object('user')
239        modify.modify_unix_user({'full_name': 'New Name',
240                                 'group_id': '25'})
241        assert modify.server.xml_in['user-name'] == self.mock_user['name']
242        assert modify.server.xml_in['full-name'] == 'New Name'
243        assert modify.server.xml_in['group-id'] == '25'
244
245    @patch('ansible.modules.storage.netapp.na_ontap_unix_user.NetAppOntapUnixUser.create_unix_user')
246    @patch('ansible.modules.storage.netapp.na_ontap_unix_user.NetAppOntapUnixUser.delete_unix_user')
247    @patch('ansible.modules.storage.netapp.na_ontap_unix_user.NetAppOntapUnixUser.modify_unix_user')
248    def test_do_nothing(self, modify, delete, create):
249        ''' changed is False and none of the opetaion methods are called'''
250        data = self.mock_args()
251        data['state'] = 'absent'
252        set_module_args(data)
253        obj = self.get_user_mock_object()
254        with pytest.raises(AnsibleExitJson) as exc:
255            obj.apply()
256        create.assert_not_called()
257        delete.assert_not_called()
258        modify.assert_not_called()
259
260    def test_get_exception(self):
261        set_module_args(self.mock_args())
262        with pytest.raises(AnsibleFailJson) as exc:
263            self.get_user_mock_object('user-fail').get_unix_user()
264        assert 'Error getting UNIX user' in exc.value.args[0]['msg']
265
266    def test_create_exception(self):
267        set_module_args(self.mock_args())
268        with pytest.raises(AnsibleFailJson) as exc:
269            self.get_user_mock_object('user-fail').create_unix_user()
270        assert 'Error creating UNIX user' in exc.value.args[0]['msg']
271
272    def test_modify_exception(self):
273        set_module_args(self.mock_args())
274        with pytest.raises(AnsibleFailJson) as exc:
275            self.get_user_mock_object('user-fail').modify_unix_user({'id': '123'})
276        assert 'Error modifying UNIX user' in exc.value.args[0]['msg']
277
278    def test_delete_exception(self):
279        set_module_args(self.mock_args())
280        with pytest.raises(AnsibleFailJson) as exc:
281            self.get_user_mock_object('user-fail').delete_unix_user()
282        assert 'Error removing UNIX user' in exc.value.args[0]['msg']
283