d1b8223255cb665a7ef486dca871bd3ea80ec7fe
[ric-plt/sdlpy.git] / ricsdl-package / tests / backend / test_redis.py
1 # Copyright (c) 2019 AT&T Intellectual Property.
2 # Copyright (c) 2018-2019 Nokia.
3 #
4 # Licensed under the Apache License, Version 2.0 (the "License");
5 # you may not use this file except in compliance with the License.
6 # You may obtain a copy of the License at
7 #
8 #     http://www.apache.org/licenses/LICENSE-2.0
9 #
10 # Unless required by applicable law or agreed to in writing, software
11 # distributed under the License is distributed on an "AS IS" BASIS,
12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 # See the License for the specific language governing permissions and
14 # limitations under the License.
15
16 #
17 # This source code is part of the near-RT RIC (RAN Intelligent Controller)
18 # platform project (RICP).
19 #
20
21
22 from unittest.mock import patch, Mock
23 import pytest
24 from redis import exceptions as redis_exceptions
25 import ricsdl.backend
26 from ricsdl.backend.redis import (RedisBackendLock, _map_to_sdl_exception)
27 from ricsdl.configuration import _Configuration
28 import ricsdl.exceptions
29
30
31 @pytest.fixture()
32 def redis_backend_fixture(request):
33     request.cls.ns = 'some-ns'
34     request.cls.dl_redis = [b'1', b'2']
35     request.cls.dm = {'a': b'1', 'b': b'2'}
36     request.cls.dm_redis = {'{some-ns},a': b'1', '{some-ns},b': b'2'}
37     request.cls.key = 'a'
38     request.cls.key_redis = '{some-ns},a'
39     request.cls.keys = ['a', 'b']
40     request.cls.keys_redis = ['{some-ns},a', '{some-ns},b']
41     request.cls.data = b'123'
42     request.cls.old_data = b'1'
43     request.cls.new_data = b'3'
44     request.cls.keyprefix = 'x?'
45     request.cls.keyprefix_redis = r'{some-ns},x\?*'
46     request.cls.matchedkeys = ['x1', 'x2', 'x3', 'x4', 'x5']
47     request.cls.matchedkeys_redis = [b'{some-ns},x1', b'{some-ns},x2', b'{some-ns},x3',
48                                      b'{some-ns},x4', b'{some-ns},x5']
49     request.cls.matcheddata_dl_redis = [b'10', b'11', b'12', b'13', b'14']
50     request.cls.matcheddata_dm = {'x1': b'10', 'x2': b'11', 'x3': b'12',
51                                   'x4': b'13', 'x5': b'14'}
52     request.cls.group = 'some-group'
53     request.cls.group_redis = '{some-ns},some-group'
54     request.cls.groupmembers = set([b'm1', b'm2'])
55     request.cls.groupmember = b'm1'
56     request.cls.is_atomic = True
57
58     request.cls.configuration = Mock()
59     mock_conf_params = _Configuration.Params(db_host=None,
60                                              db_port=None,
61                                              db_sentinel_port=None,
62                                              db_sentinel_master_name=None)
63     request.cls.configuration.get_params.return_value = mock_conf_params
64     with patch('ricsdl.backend.redis.Redis') as mock_redis:
65         db = ricsdl.backend.get_backend_instance(request.cls.configuration)
66         request.cls.mock_redis = mock_redis.return_value
67     request.cls.db = db
68
69     yield
70
71
72 @pytest.mark.usefixtures('redis_backend_fixture')
73 class TestRedisBackend:
74     def test_set_function_success(self):
75         self.db.set(self.ns, self.dm)
76         self.mock_redis.mset.assert_called_once_with(self.dm_redis)
77
78     def test_set_function_can_map_redis_exception_to_sdl_exception(self):
79         self.mock_redis.mset.side_effect = redis_exceptions.ResponseError('redis error!')
80         with pytest.raises(ricsdl.exceptions.RejectedByBackend):
81             self.db.set(self.ns, self.dm)
82
83     def test_set_if_function_success(self):
84         self.mock_redis.execute_command.return_value = True
85         ret = self.db.set_if(self.ns, self.key, self.old_data, self.new_data)
86         self.mock_redis.execute_command.assert_called_once_with('SETIE', self.key_redis,
87                                                                 self.new_data, self.old_data)
88         assert ret is True
89
90     def test_set_if_function_returns_false_if_same_data_already_exists(self):
91         self.mock_redis.execute_command.return_value = False
92         ret = self.db.set_if(self.ns, self.key, self.old_data, self.new_data)
93         self.mock_redis.execute_command.assert_called_once_with('SETIE', self.key_redis,
94                                                                 self.new_data, self.old_data)
95         assert ret is False
96
97     def test_set_if_function_can_map_redis_exception_to_sdl_exception(self):
98         self.mock_redis.execute_command.side_effect = redis_exceptions.ResponseError('redis error!')
99         with pytest.raises(ricsdl.exceptions.RejectedByBackend):
100             self.db.set_if(self.ns, self.key, self.old_data, self.new_data)
101
102     def test_set_if_not_exists_function_success(self):
103         self.mock_redis.setnx.return_value = True
104         ret = self.db.set_if_not_exists(self.ns, self.key, self.new_data)
105         self.mock_redis.setnx.assert_called_once_with(self.key_redis, self.new_data)
106         assert ret is True
107
108     def test_set_if_not_exists_function_returns_false_if_key_already_exists(self):
109         self.mock_redis.setnx.return_value = False
110         ret = self.db.set_if_not_exists(self.ns, self.key, self.new_data)
111         self.mock_redis.setnx.assert_called_once_with(self.key_redis, self.new_data)
112         assert ret is False
113
114     def test_set_if_not_exists_function_can_map_redis_exception_to_sdl_exception(self):
115         self.mock_redis.setnx.side_effect = redis_exceptions.ResponseError('redis error!')
116         with pytest.raises(ricsdl.exceptions.RejectedByBackend):
117             self.db.set_if_not_exists(self.ns, self.key, self.new_data)
118
119     def test_get_function_success(self):
120         self.mock_redis.mget.return_value = self.dl_redis
121         ret = self.db.get(self.ns, self.keys)
122         self.mock_redis.mget.assert_called_once_with(self.keys_redis)
123         assert ret == self.dm
124
125     def test_get_function_returns_empty_dict_when_no_key_values_exist(self):
126         self.mock_redis.mget.return_value = [None, None]
127         ret = self.db.get(self.ns, self.keys)
128         self.mock_redis.mget.assert_called_once_with(self.keys_redis)
129         assert ret == dict()
130
131     def test_get_function_returns_dict_only_with_found_key_values_when_some_keys_exist(self):
132         self.mock_redis.mget.return_value = [self.data, None]
133         ret = self.db.get(self.ns, self.keys)
134         self.mock_redis.mget.assert_called_once_with(self.keys_redis)
135         assert ret == {self.key: self.data}
136
137     def test_get_function_can_map_redis_exception_to_sdl_exception(self):
138         self.mock_redis.mget.side_effect = redis_exceptions.ResponseError('redis error!')
139         with pytest.raises(ricsdl.exceptions.RejectedByBackend):
140             self.db.get(self.ns, self.keys)
141
142     def test_find_keys_function_success(self):
143         self.mock_redis.keys.return_value = self.matchedkeys_redis
144         ret = self.db.find_keys(self.ns, self.keyprefix)
145         self.mock_redis.keys.assert_called_once_with(self.keyprefix_redis)
146         assert ret == self.matchedkeys
147
148     def test_find_keys_function_returns_empty_list_when_no_matching_keys_found(self):
149         self.mock_redis.keys.return_value = []
150         ret = self.db.find_keys(self.ns, self.keyprefix)
151         self.mock_redis.keys.assert_called_once_with(self.keyprefix_redis)
152         assert ret == []
153
154     def test_find_keys_function_can_map_redis_exception_to_sdl_exception(self):
155         self.mock_redis.keys.side_effect = redis_exceptions.ResponseError('redis error!')
156         with pytest.raises(ricsdl.exceptions.RejectedByBackend):
157             self.db.find_keys(self.ns, self.keyprefix)
158
159     def test_find_and_get_function_success(self):
160         self.mock_redis.keys.return_value = self.matchedkeys_redis
161         self.mock_redis.mget.return_value = self.matcheddata_dl_redis
162         ret = self.db.find_and_get(self.ns, self.keyprefix, self.is_atomic)
163         self.mock_redis.keys.assert_called_once_with(self.keyprefix_redis)
164         self.mock_redis.mget.assert_called_once_with([i.decode() for i in self.matchedkeys_redis])
165         assert ret == self.matcheddata_dm
166
167     def test_find_and_get_function_returns_empty_dict_when_no_matching_keys_exist(self):
168         self.mock_redis.keys.return_value = list()
169         ret = self.db.find_and_get(self.ns, self.keyprefix, self.is_atomic)
170         self.mock_redis.keys.assert_called_once_with(self.keyprefix_redis)
171         assert not self.mock_redis.mget.called
172         assert ret == dict()
173
174     def test_remove_function_success(self):
175         self.db.remove(self.ns, self.keys)
176         self.mock_redis.delete.assert_called_once_with(*self.keys_redis)
177
178     def test_remove_function_can_map_redis_exception_to_sdl_exception(self):
179         self.mock_redis.delete.side_effect = redis_exceptions.ResponseError('redis error!')
180         with pytest.raises(ricsdl.exceptions.RejectedByBackend):
181             self.db.remove(self.ns, self.keys)
182
183     def test_remove_if_function_success(self):
184         self.mock_redis.execute_command.return_value = True
185         ret = self.db.remove_if(self.ns, self.key, self.new_data)
186         self.mock_redis.execute_command.assert_called_once_with('DELIE', self.key_redis,
187                                                                 self.new_data)
188         assert ret is True
189
190     def test_remove_if_function_returns_false_if_data_does_not_match(self):
191         self.mock_redis.execute_command.return_value = False
192         ret = self.db.remove_if(self.ns, self.key, self.new_data)
193         self.mock_redis.execute_command.assert_called_once_with('DELIE', self.key_redis,
194                                                                 self.new_data)
195         assert ret is False
196
197     def test_remove_if_function_can_map_redis_exception_to_sdl_exception(self):
198         self.mock_redis.execute_command.side_effect = redis_exceptions.ResponseError('redis error!')
199         with pytest.raises(ricsdl.exceptions.RejectedByBackend):
200             self.db.remove_if(self.ns, self.key, self.new_data)
201
202     def test_add_member_function_success(self):
203         self.db.add_member(self.ns, self.group, self.groupmembers)
204         self.mock_redis.sadd.assert_called_once_with(self.group_redis, *self.groupmembers)
205
206     def test_add_member_function_can_map_redis_exception_to_sdl_exception(self):
207         self.mock_redis.sadd.side_effect = redis_exceptions.ResponseError('redis error!')
208         with pytest.raises(ricsdl.exceptions.RejectedByBackend):
209             self.db.add_member(self.ns, self.group, self.groupmembers)
210
211     def test_remove_member_function_success(self):
212         self.db.remove_member(self.ns, self.group, self.groupmembers)
213         self.mock_redis.srem.assert_called_once_with(self.group_redis, *self.groupmembers)
214
215     def test_remove_member_function_can_map_redis_exception_to_sdl_exception(self):
216         self.mock_redis.srem.side_effect = redis_exceptions.ResponseError('redis error!')
217         with pytest.raises(ricsdl.exceptions.RejectedByBackend):
218             self.db.remove_member(self.ns, self.group, self.groupmembers)
219
220     def test_remove_group_function_success(self):
221         self.db.remove_group(self.ns, self.group)
222         self.mock_redis.delete.assert_called_once_with(self.group_redis)
223
224     def test_remove_group_function_can_map_redis_exception_to_sdl_exception(self):
225         self.mock_redis.delete.side_effect = redis_exceptions.ResponseError('redis error!')
226         with pytest.raises(ricsdl.exceptions.RejectedByBackend):
227             self.db.remove_group(self.ns, self.group)
228
229     def test_get_members_function_success(self):
230         self.mock_redis.smembers.return_value = self.groupmembers
231         ret = self.db.get_members(self.ns, self.group)
232         self.mock_redis.smembers.assert_called_once_with(self.group_redis)
233         assert ret is self.groupmembers
234
235     def test_get_members_function_can_map_redis_exception_to_sdl_exception(self):
236         self.mock_redis.smembers.side_effect = redis_exceptions.ResponseError('redis error!')
237         with pytest.raises(ricsdl.exceptions.RejectedByBackend):
238             self.db.get_members(self.ns, self.group)
239
240     def test_is_member_function_success(self):
241         self.mock_redis.sismember.return_value = True
242         ret = self.db.is_member(self.ns, self.group, self.groupmember)
243         self.mock_redis.sismember.assert_called_once_with(self.group_redis, self.groupmember)
244         assert ret is True
245
246     def test_is_member_function_can_map_redis_exception_to_sdl_exception(self):
247         self.mock_redis.sismember.side_effect = redis_exceptions.ResponseError('redis error!')
248         with pytest.raises(ricsdl.exceptions.RejectedByBackend):
249             self.db.is_member(self.ns, self.group, self.groupmember)
250
251     def test_group_size_function_success(self):
252         self.mock_redis.scard.return_value = 100
253         ret = self.db.group_size(self.ns, self.group)
254         self.mock_redis.scard.assert_called_once_with(self.group_redis)
255         assert ret == 100
256
257     def test_group_size_function_can_map_redis_exception_to_sdl_exception(self):
258         self.mock_redis.scard.side_effect = redis_exceptions.ResponseError('Some redis error!')
259         with pytest.raises(ricsdl.exceptions.RejectedByBackend):
260             self.db.group_size(self.ns, self.group)
261
262     def test_get_redis_connection_function_success(self):
263         ret = self.db.get_redis_connection()
264         assert ret is self.mock_redis
265
266     def test_redis_backend_object_string_representation(self):
267         str_out = str(self.db)
268         assert str_out is not None
269
270
271 class MockRedisLock:
272     def __init__(self, redis, name, timeout=None, sleep=0.1,
273                  blocking=True, blocking_timeout=None, thread_local=True):
274         self.redis = redis
275         self.name = name
276         self.timeout = timeout
277         self.sleep = sleep
278         self.blocking = blocking
279         self.blocking_timeout = blocking_timeout
280         self.thread_local = bool(thread_local)
281
282
283 @pytest.fixture(scope="module")
284 def mock_redis_lock():
285     def _mock_redis_lock(name, timeout=None, sleep=0.1,
286                          blocking=True, blocking_timeout=None, thread_local=True):
287         return MockRedisLock(name, timeout, sleep, blocking, blocking_timeout, thread_local)
288     return _mock_redis_lock
289
290
291 @pytest.fixture()
292 def redis_backend_lock_fixture(request, mock_redis_lock):
293     request.cls.ns = 'some-ns'
294     request.cls.lockname = 'some-lock-name'
295     request.cls.lockname_redis = '{some-ns},some-lock-name'
296     request.cls.expiration = 10
297     request.cls.retry_interval = 0.1
298     request.cls.retry_timeout = 1
299
300     request.cls.mock_lua_get_validity_time = Mock()
301     request.cls.mock_lua_get_validity_time.return_value = 2000
302
303     request.cls.mock_redis = Mock()
304     request.cls.mock_redis.register_script = Mock()
305     request.cls.mock_redis.register_script.return_value = request.cls.mock_lua_get_validity_time
306
307     mocked_dbbackend = Mock()
308     mocked_dbbackend.get_redis_connection.return_value = request.cls.mock_redis
309     with patch('ricsdl.backend.redis.Lock') as mock_redis_lock:
310         lock = ricsdl.backend.get_backend_lock_instance(request.cls.ns, request.cls.lockname,
311                                                         request.cls.expiration, mocked_dbbackend)
312         request.cls.mock_redis_lock = mock_redis_lock.return_value
313         request.cls.lock = lock
314     yield
315     RedisBackendLock.lua_get_validity_time = None
316
317
318 @pytest.mark.usefixtures('redis_backend_lock_fixture')
319 class TestRedisBackendLock:
320     def test_acquire_function_success(self):
321         self.lock.acquire(self.retry_interval, self.retry_timeout)
322         self.mock_redis_lock.acquire.assert_called_once_with(blocking_timeout=self.retry_timeout)
323
324     def test_acquire_function_can_map_redis_exception_to_sdl_exception(self):
325         self.mock_redis_lock.acquire.side_effect = redis_exceptions.LockError('redis lock error!')
326         with pytest.raises(ricsdl.exceptions.BackendError):
327             self.lock.acquire(self.retry_interval, self.retry_timeout)
328
329     def test_release_function_success(self):
330         self.lock.release()
331         self.mock_redis_lock.release.assert_called_once()
332
333     def test_release_function_can_map_redis_exception_to_sdl_exception(self):
334         self.mock_redis_lock.release.side_effect = redis_exceptions.LockError('redis lock error!')
335         with pytest.raises(ricsdl.exceptions.BackendError):
336             self.lock.release()
337
338     def test_refresh_function_success(self):
339         self.lock.refresh()
340         self.mock_redis_lock.reacquire.assert_called_once()
341
342     def test_refresh_function_can_map_redis_exception_to_sdl_exception(self):
343         self.mock_redis_lock.reacquire.side_effect = redis_exceptions.LockError('redis lock error!')
344         with pytest.raises(ricsdl.exceptions.BackendError):
345             self.lock.refresh()
346
347     def test_get_validity_time_function_success(self):
348         self.mock_redis_lock.name = self.lockname_redis
349         self.mock_redis_lock.local.token = 123
350
351         ret = self.lock.get_validity_time()
352         self.mock_lua_get_validity_time.assert_called_once_with(
353             keys=[self.lockname_redis], args=[123], client=self.mock_redis)
354         assert ret == 2
355
356     def test_get_validity_time_function_can_raise_exception_if_lock_is_unlocked(self):
357         self.mock_redis_lock.name = self.lockname_redis
358         self.mock_redis_lock.local.token = None
359
360         with pytest.raises(ricsdl.exceptions.RejectedByBackend) as excinfo:
361             self.lock.get_validity_time()
362         assert f"Cannot get validity time of an unlocked lock {self.lockname}" in str(excinfo.value)
363
364     def test_get_validity_time_function_can_raise_exception_if_lua_script_fails(self):
365         self.mock_redis_lock.name = self.lockname_redis
366         self.mock_redis_lock.local.token = 123
367         self.mock_lua_get_validity_time.return_value = -10
368
369         with pytest.raises(ricsdl.exceptions.RejectedByBackend) as excinfo:
370             self.lock.get_validity_time()
371         assert f"Getting validity time of a lock {self.lockname} failed with error code: -10" in str(excinfo.value)
372
373     def test_redis_backend_lock_object_string_representation(self):
374         str_out = str(self.lock)
375         assert str_out is not None
376
377
378 def test_redis_response_error_exception_is_mapped_to_rejected_by_backend_sdl_exception():
379     with pytest.raises(ricsdl.exceptions.RejectedByBackend) as excinfo:
380         with _map_to_sdl_exception():
381             raise redis_exceptions.ResponseError('Some redis error!')
382     assert "SDL backend rejected the request: Some redis error!" in str(excinfo.value)
383
384
385 def test_redis_connection_error_exception_is_mapped_to_not_connected_sdl_exception():
386     with pytest.raises(ricsdl.exceptions.NotConnected) as excinfo:
387         with _map_to_sdl_exception():
388             raise redis_exceptions.ConnectionError('Some redis error!')
389     assert "SDL not connected to backend: Some redis error!" in str(excinfo.value)
390
391
392 def test_rest_redis_exceptions_are_mapped_to_backend_error_sdl_exception():
393     with pytest.raises(ricsdl.exceptions.BackendError) as excinfo:
394         with _map_to_sdl_exception():
395             raise redis_exceptions.RedisError('Some redis error!')
396     assert "SDL backend failed to process the request: Some redis error!" in str(excinfo.value)
397
398
399 def test_system_error_exceptions_are_not_mapped_to_any_sdl_exception():
400     with pytest.raises(SystemExit):
401         with _map_to_sdl_exception():
402             raise SystemExit('Fatal error')