Implement regular expression support for key searching
[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.keypattern = r'[Aa]bc-\[1\].?-*'
45     request.cls.keypattern_redis = r'{some-ns},[Aa]bc-\[1\].?-*'
46     request.cls.matchedkeys = ['Abc-[1].0-def', 'abc-[1].1-ghi']
47     request.cls.matchedkeys_redis = [b'{some-ns},Abc-[1].0-def',
48                                      b'{some-ns},abc-[1].1-ghi']
49     request.cls.matcheddata_redis = [b'10', b'11']
50     request.cls.matchedkeydata = {'Abc-[1].0-def': b'10',
51                                   'abc-[1].1-ghi': b'11'}
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
57     request.cls.configuration = Mock()
58     mock_conf_params = _Configuration.Params(db_host=None,
59                                              db_port=None,
60                                              db_sentinel_port=None,
61                                              db_sentinel_master_name=None)
62     request.cls.configuration.get_params.return_value = mock_conf_params
63     with patch('ricsdl.backend.redis.Redis') as mock_redis:
64         db = ricsdl.backend.get_backend_instance(request.cls.configuration)
65         request.cls.mock_redis = mock_redis.return_value
66     request.cls.db = db
67
68     yield
69
70
71 @pytest.mark.usefixtures('redis_backend_fixture')
72 class TestRedisBackend:
73     def test_set_function_success(self):
74         self.db.set(self.ns, self.dm)
75         self.mock_redis.mset.assert_called_once_with(self.dm_redis)
76
77     def test_set_function_can_map_redis_exception_to_sdl_exception(self):
78         self.mock_redis.mset.side_effect = redis_exceptions.ResponseError('redis error!')
79         with pytest.raises(ricsdl.exceptions.RejectedByBackend):
80             self.db.set(self.ns, self.dm)
81
82     def test_set_if_function_success(self):
83         self.mock_redis.execute_command.return_value = True
84         ret = self.db.set_if(self.ns, self.key, self.old_data, self.new_data)
85         self.mock_redis.execute_command.assert_called_once_with('SETIE', self.key_redis,
86                                                                 self.new_data, self.old_data)
87         assert ret is True
88
89     def test_set_if_function_returns_false_if_same_data_already_exists(self):
90         self.mock_redis.execute_command.return_value = False
91         ret = self.db.set_if(self.ns, self.key, self.old_data, self.new_data)
92         self.mock_redis.execute_command.assert_called_once_with('SETIE', self.key_redis,
93                                                                 self.new_data, self.old_data)
94         assert ret is False
95
96     def test_set_if_function_can_map_redis_exception_to_sdl_exception(self):
97         self.mock_redis.execute_command.side_effect = redis_exceptions.ResponseError('redis error!')
98         with pytest.raises(ricsdl.exceptions.RejectedByBackend):
99             self.db.set_if(self.ns, self.key, self.old_data, self.new_data)
100
101     def test_set_if_not_exists_function_success(self):
102         self.mock_redis.setnx.return_value = True
103         ret = self.db.set_if_not_exists(self.ns, self.key, self.new_data)
104         self.mock_redis.setnx.assert_called_once_with(self.key_redis, self.new_data)
105         assert ret is True
106
107     def test_set_if_not_exists_function_returns_false_if_key_already_exists(self):
108         self.mock_redis.setnx.return_value = False
109         ret = self.db.set_if_not_exists(self.ns, self.key, self.new_data)
110         self.mock_redis.setnx.assert_called_once_with(self.key_redis, self.new_data)
111         assert ret is False
112
113     def test_set_if_not_exists_function_can_map_redis_exception_to_sdl_exception(self):
114         self.mock_redis.setnx.side_effect = redis_exceptions.ResponseError('redis error!')
115         with pytest.raises(ricsdl.exceptions.RejectedByBackend):
116             self.db.set_if_not_exists(self.ns, self.key, self.new_data)
117
118     def test_get_function_success(self):
119         self.mock_redis.mget.return_value = self.dl_redis
120         ret = self.db.get(self.ns, self.keys)
121         self.mock_redis.mget.assert_called_once_with(self.keys_redis)
122         assert ret == self.dm
123
124     def test_get_function_returns_empty_dict_when_no_key_values_exist(self):
125         self.mock_redis.mget.return_value = [None, None]
126         ret = self.db.get(self.ns, self.keys)
127         self.mock_redis.mget.assert_called_once_with(self.keys_redis)
128         assert ret == dict()
129
130     def test_get_function_returns_dict_only_with_found_key_values_when_some_keys_exist(self):
131         self.mock_redis.mget.return_value = [self.data, None]
132         ret = self.db.get(self.ns, self.keys)
133         self.mock_redis.mget.assert_called_once_with(self.keys_redis)
134         assert ret == {self.key: self.data}
135
136     def test_get_function_can_map_redis_exception_to_sdl_exception(self):
137         self.mock_redis.mget.side_effect = redis_exceptions.ResponseError('redis error!')
138         with pytest.raises(ricsdl.exceptions.RejectedByBackend):
139             self.db.get(self.ns, self.keys)
140
141     def test_find_keys_function_success(self):
142         self.mock_redis.keys.return_value = self.matchedkeys_redis
143         ret = self.db.find_keys(self.ns, self.keypattern)
144         self.mock_redis.keys.assert_called_once_with(self.keypattern_redis)
145         assert ret == self.matchedkeys
146
147     def test_find_keys_function_returns_empty_list_when_no_matching_keys_found(self):
148         self.mock_redis.keys.return_value = []
149         ret = self.db.find_keys(self.ns, self.keypattern)
150         self.mock_redis.keys.assert_called_once_with(self.keypattern_redis)
151         assert ret == []
152
153     def test_find_keys_function_can_map_redis_exception_to_sdl_exception(self):
154         self.mock_redis.keys.side_effect = redis_exceptions.ResponseError('redis error!')
155         with pytest.raises(ricsdl.exceptions.RejectedByBackend):
156             self.db.find_keys(self.ns, self.keypattern)
157
158     def test_find_keys_function_can_raise_exception_when_redis_key_convert_to_string_fails(self):
159         # Redis returns an illegal key, which conversion to string fails
160         corrupt_redis_key = b'\x81'
161         self.mock_redis.keys.return_value = [corrupt_redis_key]
162         with pytest.raises(ricsdl.exceptions.RejectedByBackend) as excinfo:
163             self.db.find_keys(self.ns, self.keypattern)
164         assert f"Namespace {self.ns} key:{corrupt_redis_key} "
165         "has no namespace prefix" in str(excinfo.value)
166
167     def test_find_keys_function_can_raise_exception_when_redis_key_is_without_prefix(self):
168         # Redis returns an illegal key, which doesn't have comma separated namespace prefix
169         corrupt_redis_key = 'some-corrupt-key'
170         self.mock_redis.keys.return_value = [f'{corrupt_redis_key}'.encode()]
171         with pytest.raises(ricsdl.exceptions.RejectedByBackend) as excinfo:
172             self.db.find_keys(self.ns, self.keypattern)
173         assert f"Namespace {self.ns} key:{corrupt_redis_key} "
174         "has no namespace prefix" in str(excinfo.value)
175
176     def test_find_and_get_function_success(self):
177         self.mock_redis.keys.return_value = self.matchedkeys_redis
178         self.mock_redis.mget.return_value = self.matcheddata_redis
179         ret = self.db.find_and_get(self.ns, self.keypattern)
180         self.mock_redis.keys.assert_called_once_with(self.keypattern_redis)
181         self.mock_redis.mget.assert_called_once_with([i.decode() for i in self.matchedkeys_redis])
182         assert ret == self.matchedkeydata
183
184     def test_find_and_get_function_returns_empty_dict_when_no_matching_keys_exist(self):
185         self.mock_redis.keys.return_value = list()
186         ret = self.db.find_and_get(self.ns, self.keypattern)
187         self.mock_redis.keys.assert_called_once_with(self.keypattern_redis)
188         assert not self.mock_redis.mget.called
189         assert ret == dict()
190
191     def test_find_and_get_function_can_map_redis_exception_to_sdl_exception(self):
192         self.mock_redis.keys.side_effect = redis_exceptions.ResponseError('redis error!')
193         with pytest.raises(ricsdl.exceptions.RejectedByBackend):
194             self.db.find_and_get(self.ns, self.keypattern)
195
196     def test_find_and_get_function_can_raise_exception_when_redis_key_convert_to_string_fails(self):
197         # Redis returns an illegal key, which conversion to string fails
198         corrupt_redis_key = b'\x81'
199         self.mock_redis.keys.return_value = [corrupt_redis_key]
200         with pytest.raises(ricsdl.exceptions.RejectedByBackend) as excinfo:
201             self.db.find_and_get(self.ns, self.keypattern)
202         assert f"Namespace {self.ns} key:{corrupt_redis_key} "
203         "has no namespace prefix" in str(excinfo.value)
204
205     def test_find_and_get_function_can_raise_exception_when_redis_key_is_without_prefix(self):
206         # Redis returns an illegal key, which doesn't have comma separated namespace prefix
207         corrupt_redis_key = 'some-corrupt-key'
208         self.mock_redis.keys.return_value = [f'{corrupt_redis_key}'.encode()]
209         with pytest.raises(ricsdl.exceptions.RejectedByBackend) as excinfo:
210             self.db.find_and_get(self.ns, self.keypattern)
211         assert f"Namespace {self.ns} key:{corrupt_redis_key} "
212         "has no namespace prefix" in str(excinfo.value)
213
214     def test_remove_function_success(self):
215         self.db.remove(self.ns, self.keys)
216         self.mock_redis.delete.assert_called_once_with(*self.keys_redis)
217
218     def test_remove_function_can_map_redis_exception_to_sdl_exception(self):
219         self.mock_redis.delete.side_effect = redis_exceptions.ResponseError('redis error!')
220         with pytest.raises(ricsdl.exceptions.RejectedByBackend):
221             self.db.remove(self.ns, self.keys)
222
223     def test_remove_if_function_success(self):
224         self.mock_redis.execute_command.return_value = True
225         ret = self.db.remove_if(self.ns, self.key, self.new_data)
226         self.mock_redis.execute_command.assert_called_once_with('DELIE', self.key_redis,
227                                                                 self.new_data)
228         assert ret is True
229
230     def test_remove_if_function_returns_false_if_data_does_not_match(self):
231         self.mock_redis.execute_command.return_value = False
232         ret = self.db.remove_if(self.ns, self.key, self.new_data)
233         self.mock_redis.execute_command.assert_called_once_with('DELIE', self.key_redis,
234                                                                 self.new_data)
235         assert ret is False
236
237     def test_remove_if_function_can_map_redis_exception_to_sdl_exception(self):
238         self.mock_redis.execute_command.side_effect = redis_exceptions.ResponseError('redis error!')
239         with pytest.raises(ricsdl.exceptions.RejectedByBackend):
240             self.db.remove_if(self.ns, self.key, self.new_data)
241
242     def test_add_member_function_success(self):
243         self.db.add_member(self.ns, self.group, self.groupmembers)
244         self.mock_redis.sadd.assert_called_once_with(self.group_redis, *self.groupmembers)
245
246     def test_add_member_function_can_map_redis_exception_to_sdl_exception(self):
247         self.mock_redis.sadd.side_effect = redis_exceptions.ResponseError('redis error!')
248         with pytest.raises(ricsdl.exceptions.RejectedByBackend):
249             self.db.add_member(self.ns, self.group, self.groupmembers)
250
251     def test_remove_member_function_success(self):
252         self.db.remove_member(self.ns, self.group, self.groupmembers)
253         self.mock_redis.srem.assert_called_once_with(self.group_redis, *self.groupmembers)
254
255     def test_remove_member_function_can_map_redis_exception_to_sdl_exception(self):
256         self.mock_redis.srem.side_effect = redis_exceptions.ResponseError('redis error!')
257         with pytest.raises(ricsdl.exceptions.RejectedByBackend):
258             self.db.remove_member(self.ns, self.group, self.groupmembers)
259
260     def test_remove_group_function_success(self):
261         self.db.remove_group(self.ns, self.group)
262         self.mock_redis.delete.assert_called_once_with(self.group_redis)
263
264     def test_remove_group_function_can_map_redis_exception_to_sdl_exception(self):
265         self.mock_redis.delete.side_effect = redis_exceptions.ResponseError('redis error!')
266         with pytest.raises(ricsdl.exceptions.RejectedByBackend):
267             self.db.remove_group(self.ns, self.group)
268
269     def test_get_members_function_success(self):
270         self.mock_redis.smembers.return_value = self.groupmembers
271         ret = self.db.get_members(self.ns, self.group)
272         self.mock_redis.smembers.assert_called_once_with(self.group_redis)
273         assert ret is self.groupmembers
274
275     def test_get_members_function_can_map_redis_exception_to_sdl_exception(self):
276         self.mock_redis.smembers.side_effect = redis_exceptions.ResponseError('redis error!')
277         with pytest.raises(ricsdl.exceptions.RejectedByBackend):
278             self.db.get_members(self.ns, self.group)
279
280     def test_is_member_function_success(self):
281         self.mock_redis.sismember.return_value = True
282         ret = self.db.is_member(self.ns, self.group, self.groupmember)
283         self.mock_redis.sismember.assert_called_once_with(self.group_redis, self.groupmember)
284         assert ret is True
285
286     def test_is_member_function_can_map_redis_exception_to_sdl_exception(self):
287         self.mock_redis.sismember.side_effect = redis_exceptions.ResponseError('redis error!')
288         with pytest.raises(ricsdl.exceptions.RejectedByBackend):
289             self.db.is_member(self.ns, self.group, self.groupmember)
290
291     def test_group_size_function_success(self):
292         self.mock_redis.scard.return_value = 100
293         ret = self.db.group_size(self.ns, self.group)
294         self.mock_redis.scard.assert_called_once_with(self.group_redis)
295         assert ret == 100
296
297     def test_group_size_function_can_map_redis_exception_to_sdl_exception(self):
298         self.mock_redis.scard.side_effect = redis_exceptions.ResponseError('Some redis error!')
299         with pytest.raises(ricsdl.exceptions.RejectedByBackend):
300             self.db.group_size(self.ns, self.group)
301
302     def test_get_redis_connection_function_success(self):
303         ret = self.db.get_redis_connection()
304         assert ret is self.mock_redis
305
306     def test_redis_backend_object_string_representation(self):
307         str_out = str(self.db)
308         assert str_out is not None
309
310
311 class MockRedisLock:
312     def __init__(self, redis, name, timeout=None, sleep=0.1,
313                  blocking=True, blocking_timeout=None, thread_local=True):
314         self.redis = redis
315         self.name = name
316         self.timeout = timeout
317         self.sleep = sleep
318         self.blocking = blocking
319         self.blocking_timeout = blocking_timeout
320         self.thread_local = bool(thread_local)
321
322
323 @pytest.fixture(scope="module")
324 def mock_redis_lock():
325     def _mock_redis_lock(name, timeout=None, sleep=0.1,
326                          blocking=True, blocking_timeout=None, thread_local=True):
327         return MockRedisLock(name, timeout, sleep, blocking, blocking_timeout, thread_local)
328     return _mock_redis_lock
329
330
331 @pytest.fixture()
332 def redis_backend_lock_fixture(request, mock_redis_lock):
333     request.cls.ns = 'some-ns'
334     request.cls.lockname = 'some-lock-name'
335     request.cls.lockname_redis = '{some-ns},some-lock-name'
336     request.cls.expiration = 10
337     request.cls.retry_interval = 0.1
338     request.cls.retry_timeout = 1
339
340     request.cls.mock_lua_get_validity_time = Mock()
341     request.cls.mock_lua_get_validity_time.return_value = 2000
342
343     request.cls.mock_redis = Mock()
344     request.cls.mock_redis.register_script = Mock()
345     request.cls.mock_redis.register_script.return_value = request.cls.mock_lua_get_validity_time
346
347     mocked_dbbackend = Mock()
348     mocked_dbbackend.get_redis_connection.return_value = request.cls.mock_redis
349     with patch('ricsdl.backend.redis.Lock') as mock_redis_lock:
350         lock = ricsdl.backend.get_backend_lock_instance(request.cls.ns, request.cls.lockname,
351                                                         request.cls.expiration, mocked_dbbackend)
352         request.cls.mock_redis_lock = mock_redis_lock.return_value
353         request.cls.lock = lock
354     yield
355     RedisBackendLock.lua_get_validity_time = None
356
357
358 @pytest.mark.usefixtures('redis_backend_lock_fixture')
359 class TestRedisBackendLock:
360     def test_acquire_function_success(self):
361         self.lock.acquire(self.retry_interval, self.retry_timeout)
362         self.mock_redis_lock.acquire.assert_called_once_with(blocking_timeout=self.retry_timeout)
363
364     def test_acquire_function_can_map_redis_exception_to_sdl_exception(self):
365         self.mock_redis_lock.acquire.side_effect = redis_exceptions.LockError('redis lock error!')
366         with pytest.raises(ricsdl.exceptions.BackendError):
367             self.lock.acquire(self.retry_interval, self.retry_timeout)
368
369     def test_release_function_success(self):
370         self.lock.release()
371         self.mock_redis_lock.release.assert_called_once()
372
373     def test_release_function_can_map_redis_exception_to_sdl_exception(self):
374         self.mock_redis_lock.release.side_effect = redis_exceptions.LockError('redis lock error!')
375         with pytest.raises(ricsdl.exceptions.BackendError):
376             self.lock.release()
377
378     def test_refresh_function_success(self):
379         self.lock.refresh()
380         self.mock_redis_lock.reacquire.assert_called_once()
381
382     def test_refresh_function_can_map_redis_exception_to_sdl_exception(self):
383         self.mock_redis_lock.reacquire.side_effect = redis_exceptions.LockError('redis lock error!')
384         with pytest.raises(ricsdl.exceptions.BackendError):
385             self.lock.refresh()
386
387     def test_get_validity_time_function_success(self):
388         self.mock_redis_lock.name = self.lockname_redis
389         self.mock_redis_lock.local.token = 123
390
391         ret = self.lock.get_validity_time()
392         self.mock_lua_get_validity_time.assert_called_once_with(
393             keys=[self.lockname_redis], args=[123], client=self.mock_redis)
394         assert ret == 2
395
396     def test_get_validity_time_function_second_fraction_success(self):
397         self.mock_redis_lock.name = self.lockname_redis
398         self.mock_redis_lock.local.token = 123
399         self.mock_lua_get_validity_time.return_value = 234
400
401         ret = self.lock.get_validity_time()
402         self.mock_lua_get_validity_time.assert_called_once_with(
403             keys=[self.lockname_redis], args=[123], client=self.mock_redis)
404         assert ret == 0.234
405
406     def test_get_validity_time_function_can_raise_exception_if_lock_is_unlocked(self):
407         self.mock_redis_lock.name = self.lockname_redis
408         self.mock_redis_lock.local.token = None
409
410         with pytest.raises(ricsdl.exceptions.RejectedByBackend) as excinfo:
411             self.lock.get_validity_time()
412         assert f"Cannot get validity time of an unlocked lock {self.lockname}" in str(excinfo.value)
413
414     def test_get_validity_time_function_can_raise_exception_if_lua_script_fails(self):
415         self.mock_redis_lock.name = self.lockname_redis
416         self.mock_redis_lock.local.token = 123
417         self.mock_lua_get_validity_time.return_value = -10
418
419         with pytest.raises(ricsdl.exceptions.RejectedByBackend) as excinfo:
420             self.lock.get_validity_time()
421         assert f"Getting validity time of a lock {self.lockname} failed with error code: -10" in str(excinfo.value)
422
423     def test_redis_backend_lock_object_string_representation(self):
424         expected_lock_info = {'lock namespace': 'some-ns',
425                               'lock name': 'some-lock-name',
426                               'lock status': 'locked'}
427         assert str(self.lock) == str(expected_lock_info)
428
429     def test_redis_backend_lock_object_string_representation_can_catch_redis_exception(self):
430         self.mock_redis_lock.owned.side_effect = redis_exceptions.LockError('redis lock error!')
431         expected_lock_info = {'lock namespace': 'some-ns',
432                               'lock name': 'some-lock-name',
433                               'lock status': 'Error: redis lock error!'}
434         assert str(self.lock) == str(expected_lock_info)
435
436
437 def test_redis_response_error_exception_is_mapped_to_rejected_by_backend_sdl_exception():
438     with pytest.raises(ricsdl.exceptions.RejectedByBackend) as excinfo:
439         with _map_to_sdl_exception():
440             raise redis_exceptions.ResponseError('Some redis error!')
441     assert "SDL backend rejected the request: Some redis error!" in str(excinfo.value)
442
443
444 def test_redis_connection_error_exception_is_mapped_to_not_connected_sdl_exception():
445     with pytest.raises(ricsdl.exceptions.NotConnected) as excinfo:
446         with _map_to_sdl_exception():
447             raise redis_exceptions.ConnectionError('Some redis error!')
448     assert "SDL not connected to backend: Some redis error!" in str(excinfo.value)
449
450
451 def test_rest_redis_exceptions_are_mapped_to_backend_error_sdl_exception():
452     with pytest.raises(ricsdl.exceptions.BackendError) as excinfo:
453         with _map_to_sdl_exception():
454             raise redis_exceptions.RedisError('Some redis error!')
455     assert "SDL backend failed to process the request: Some redis error!" in str(excinfo.value)
456
457
458 def test_system_error_exceptions_are_not_mapped_to_any_sdl_exception():
459     with pytest.raises(SystemExit):
460         with _map_to_sdl_exception():
461             raise SystemExit('Fatal error')