mirror of
https://github.com/GSA/notifications-api.git
synced 2026-02-02 17:31:14 -05:00
Removed one more utils reference in the README and fixed directory name
Signed-off-by: Carlo Costino <carlo.costino@gsa.gov>
This commit is contained in:
221
tests/notifications_utils/clients/redis/test_redis_client.py
Normal file
221
tests/notifications_utils/clients/redis/test_redis_client.py
Normal file
@@ -0,0 +1,221 @@
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from unittest.mock import Mock, call
|
||||
|
||||
import pytest
|
||||
from freezegun import freeze_time
|
||||
|
||||
from notifications_utils.clients.redis.redis_client import RedisClient, prepare_value
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def mocked_redis_pipeline():
|
||||
return Mock()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def delete_mock():
|
||||
return Mock(return_value=4)
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def mocked_redis_client(app, mocked_redis_pipeline, delete_mock, mocker):
|
||||
app.config["REDIS_ENABLED"] = True
|
||||
|
||||
redis_client = RedisClient()
|
||||
redis_client.init_app(app)
|
||||
|
||||
mocker.patch.object(redis_client.redis_store, "get", return_value=100)
|
||||
mocker.patch.object(redis_client.redis_store, "set")
|
||||
mocker.patch.object(redis_client.redis_store, "incr")
|
||||
mocker.patch.object(redis_client.redis_store, "delete")
|
||||
mocker.patch.object(
|
||||
redis_client.redis_store, "pipeline", return_value=mocked_redis_pipeline
|
||||
)
|
||||
|
||||
mocker.patch.object(
|
||||
redis_client, "scripts", {"delete-keys-by-pattern": delete_mock}
|
||||
)
|
||||
|
||||
mocker.patch.object(
|
||||
redis_client.redis_store,
|
||||
"hgetall",
|
||||
return_value={b"template-1111": b"8", b"template-2222": b"8"},
|
||||
)
|
||||
|
||||
return redis_client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def failing_redis_client(mocked_redis_client, delete_mock):
|
||||
mocked_redis_client.redis_store.get.side_effect = Exception("get failed")
|
||||
mocked_redis_client.redis_store.set.side_effect = Exception("set failed")
|
||||
mocked_redis_client.redis_store.incr.side_effect = Exception("incr failed")
|
||||
mocked_redis_client.redis_store.pipeline.side_effect = Exception("pipeline failed")
|
||||
mocked_redis_client.redis_store.delete.side_effect = Exception("delete failed")
|
||||
delete_mock.side_effect = Exception("delete by pattern failed")
|
||||
return mocked_redis_client
|
||||
|
||||
|
||||
def test_should_not_raise_exception_if_raise_set_to_false(
|
||||
app, caplog, failing_redis_client, mocker
|
||||
):
|
||||
mock_logger = mocker.patch("flask.Flask.logger")
|
||||
|
||||
assert failing_redis_client.get("get_key") is None
|
||||
assert failing_redis_client.set("set_key", "set_value") is None
|
||||
assert failing_redis_client.incr("incr_key") is None
|
||||
assert failing_redis_client.exceeded_rate_limit("rate_limit_key", 100, 100) is False
|
||||
assert failing_redis_client.delete("delete_key") is None
|
||||
assert failing_redis_client.delete("a", "b", "c") is None
|
||||
assert failing_redis_client.delete_by_pattern("pattern") == 0
|
||||
|
||||
assert mock_logger.mock_calls == [
|
||||
call.exception("Redis error performing get on get_key"),
|
||||
call.exception("Redis error performing set on set_key"),
|
||||
call.exception("Redis error performing incr on incr_key"),
|
||||
call.exception("Redis error performing rate-limit-pipeline on rate_limit_key"),
|
||||
call.exception("Redis error performing delete on delete_key"),
|
||||
call.exception("Redis error performing delete on a, b, c"),
|
||||
call.exception("Redis error performing delete-by-pattern on pattern"),
|
||||
]
|
||||
|
||||
|
||||
def test_should_raise_exception_if_raise_set_to_true(
|
||||
app,
|
||||
failing_redis_client,
|
||||
):
|
||||
with pytest.raises(Exception) as e:
|
||||
failing_redis_client.get("test", raise_exception=True)
|
||||
assert str(e.value) == "get failed"
|
||||
|
||||
with pytest.raises(Exception) as e:
|
||||
failing_redis_client.set("test", "test", raise_exception=True)
|
||||
assert str(e.value) == "set failed"
|
||||
|
||||
with pytest.raises(Exception) as e:
|
||||
failing_redis_client.incr("test", raise_exception=True)
|
||||
assert str(e.value) == "incr failed"
|
||||
|
||||
with pytest.raises(Exception) as e:
|
||||
failing_redis_client.exceeded_rate_limit("test", 100, 200, raise_exception=True)
|
||||
assert str(e.value) == "pipeline failed"
|
||||
|
||||
with pytest.raises(Exception) as e:
|
||||
failing_redis_client.delete("test", raise_exception=True)
|
||||
assert str(e.value) == "delete failed"
|
||||
|
||||
with pytest.raises(Exception) as e:
|
||||
failing_redis_client.delete_by_pattern("pattern", raise_exception=True)
|
||||
assert str(e.value) == "delete by pattern failed"
|
||||
|
||||
|
||||
def test_should_not_call_if_not_enabled(mocked_redis_client, delete_mock):
|
||||
mocked_redis_client.active = False
|
||||
|
||||
assert mocked_redis_client.get("get_key") is None
|
||||
assert mocked_redis_client.set("set_key", "set_value") is None
|
||||
assert mocked_redis_client.incr("incr_key") is None
|
||||
assert mocked_redis_client.exceeded_rate_limit("rate_limit_key", 100, 100) is False
|
||||
assert mocked_redis_client.delete("delete_key") is None
|
||||
assert mocked_redis_client.delete_by_pattern("pattern") == 0
|
||||
|
||||
mocked_redis_client.redis_store.get.assert_not_called()
|
||||
mocked_redis_client.redis_store.set.assert_not_called()
|
||||
mocked_redis_client.redis_store.incr.assert_not_called()
|
||||
mocked_redis_client.redis_store.delete.assert_not_called()
|
||||
mocked_redis_client.redis_store.pipeline.assert_not_called()
|
||||
delete_mock.assert_not_called()
|
||||
|
||||
|
||||
def test_should_call_set_if_enabled(mocked_redis_client):
|
||||
mocked_redis_client.set("key", "value")
|
||||
mocked_redis_client.redis_store.set.assert_called_with(
|
||||
"key", "value", None, None, False, False
|
||||
)
|
||||
|
||||
|
||||
def test_should_call_get_if_enabled(mocked_redis_client):
|
||||
assert mocked_redis_client.get("key") == 100
|
||||
mocked_redis_client.redis_store.get.assert_called_with("key")
|
||||
|
||||
|
||||
@freeze_time("2001-01-01 12:00:00.000000")
|
||||
def test_exceeded_rate_limit_should_add_correct_calls_to_the_pipe(
|
||||
mocked_redis_client, mocked_redis_pipeline
|
||||
):
|
||||
mocked_redis_client.exceeded_rate_limit("key", 100, 100)
|
||||
assert mocked_redis_client.redis_store.pipeline.called
|
||||
mocked_redis_pipeline.zadd.assert_called_with("key", {978350400.0: 978350400.0})
|
||||
mocked_redis_pipeline.zremrangebyscore.assert_called_with(
|
||||
"key", "-inf", 978350300.0
|
||||
)
|
||||
mocked_redis_pipeline.zcard.assert_called_with("key")
|
||||
mocked_redis_pipeline.expire.assert_called_with("key", 100)
|
||||
assert mocked_redis_pipeline.execute.called
|
||||
|
||||
|
||||
@freeze_time("2001-01-01 12:00:00.000000")
|
||||
def test_exceeded_rate_limit_should_fail_request_if_over_limit(
|
||||
mocked_redis_client, mocked_redis_pipeline
|
||||
):
|
||||
mocked_redis_pipeline.execute.return_value = [True, True, 100, True]
|
||||
assert mocked_redis_client.exceeded_rate_limit("key", 99, 100)
|
||||
|
||||
|
||||
@freeze_time("2001-01-01 12:00:00.000000")
|
||||
def test_exceeded_rate_limit_should_allow_request_if_not_over_limit(
|
||||
mocked_redis_client, mocked_redis_pipeline
|
||||
):
|
||||
mocked_redis_pipeline.execute.return_value = [True, True, 100, True]
|
||||
assert not mocked_redis_client.exceeded_rate_limit("key", 101, 100)
|
||||
|
||||
|
||||
@freeze_time("2001-01-01 12:00:00.000000")
|
||||
def test_exceeded_rate_limit_not_exceeded(mocked_redis_client, mocked_redis_pipeline):
|
||||
mocked_redis_pipeline.execute.return_value = [True, True, 80, True]
|
||||
assert not mocked_redis_client.exceeded_rate_limit("key", 90, 100)
|
||||
|
||||
|
||||
def test_exceeded_rate_limit_should_not_call_if_not_enabled(
|
||||
mocked_redis_client, mocked_redis_pipeline
|
||||
):
|
||||
mocked_redis_client.active = False
|
||||
|
||||
assert not mocked_redis_client.exceeded_rate_limit("key", 100, 100)
|
||||
assert not mocked_redis_client.redis_store.pipeline.called
|
||||
|
||||
|
||||
def test_delete(mocked_redis_client):
|
||||
key = "hash-key"
|
||||
mocked_redis_client.delete(key)
|
||||
mocked_redis_client.redis_store.delete.assert_called_with(key)
|
||||
|
||||
|
||||
def test_delete_multi(mocked_redis_client):
|
||||
mocked_redis_client.delete("a", "b", "c")
|
||||
mocked_redis_client.redis_store.delete.assert_called_with("a", "b", "c")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"input,output",
|
||||
[
|
||||
(b"asdf", b"asdf"),
|
||||
("asdf", "asdf"),
|
||||
(0, 0),
|
||||
(1.2, 1.2),
|
||||
(uuid.UUID(int=0), "00000000-0000-0000-0000-000000000000"),
|
||||
pytest.param({"a": 1}, None, marks=pytest.mark.xfail(raises=ValueError)),
|
||||
pytest.param(
|
||||
datetime.utcnow(), None, marks=pytest.mark.xfail(raises=ValueError)
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_prepare_value(input, output):
|
||||
assert prepare_value(input) == output
|
||||
|
||||
|
||||
def test_delete_by_pattern(mocked_redis_client, delete_mock):
|
||||
ret = mocked_redis_client.delete_by_pattern("foo")
|
||||
assert ret == 4
|
||||
delete_mock.assert_called_once_with(args=["foo"])
|
||||
190
tests/notifications_utils/clients/redis/test_request_cache.py
Normal file
190
tests/notifications_utils/clients/redis/test_request_cache.py
Normal file
@@ -0,0 +1,190 @@
|
||||
import pytest
|
||||
|
||||
from notifications_utils.clients.redis import RequestCache
|
||||
from notifications_utils.clients.redis.redis_client import RedisClient
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def mocked_redis_client(app):
|
||||
app.config["REDIS_ENABLED"] = True
|
||||
redis_client = RedisClient()
|
||||
redis_client.init_app(app)
|
||||
return redis_client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def cache(mocked_redis_client):
|
||||
return RequestCache(mocked_redis_client)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"args, kwargs, expected_cache_key",
|
||||
(
|
||||
([1, 2, 3], {}, "1-2-3-None-None-None"),
|
||||
([1, 2, 3, 4, 5, 6], {}, "1-2-3-4-5-6"),
|
||||
([1, 2, 3], {"x": 4, "y": 5, "z": 6}, "1-2-3-4-5-6"),
|
||||
([1, 2, 3, 4], {"y": 5}, "1-2-3-4-5-None"),
|
||||
),
|
||||
)
|
||||
def test_set(
|
||||
mocker,
|
||||
mocked_redis_client,
|
||||
cache,
|
||||
args,
|
||||
kwargs,
|
||||
expected_cache_key,
|
||||
):
|
||||
mock_redis_set = mocker.patch.object(
|
||||
mocked_redis_client,
|
||||
"set",
|
||||
)
|
||||
mock_redis_get = mocker.patch.object(
|
||||
mocked_redis_client,
|
||||
"get",
|
||||
return_value=None,
|
||||
)
|
||||
|
||||
@cache.set("{a}-{b}-{c}-{x}-{y}-{z}")
|
||||
def foo(a, b, c, x=None, y=None, z=None):
|
||||
return "bar"
|
||||
|
||||
assert foo(*args, **kwargs) == "bar"
|
||||
|
||||
mock_redis_get.assert_called_once_with(expected_cache_key)
|
||||
|
||||
mock_redis_set.assert_called_once_with(
|
||||
expected_cache_key,
|
||||
'"bar"',
|
||||
ex=604_800,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"cache_set_call, expected_redis_client_ttl",
|
||||
(
|
||||
(0, 0),
|
||||
(1, 1),
|
||||
(1.111, 1),
|
||||
("2000", 2_000),
|
||||
),
|
||||
)
|
||||
def test_set_with_custom_ttl(
|
||||
mocker,
|
||||
mocked_redis_client,
|
||||
cache,
|
||||
cache_set_call,
|
||||
expected_redis_client_ttl,
|
||||
):
|
||||
mock_redis_set = mocker.patch.object(
|
||||
mocked_redis_client,
|
||||
"set",
|
||||
)
|
||||
mocker.patch.object(
|
||||
mocked_redis_client,
|
||||
"get",
|
||||
return_value=None,
|
||||
)
|
||||
|
||||
@cache.set("foo", ttl_in_seconds=cache_set_call)
|
||||
def foo():
|
||||
return "bar"
|
||||
|
||||
foo()
|
||||
|
||||
mock_redis_set.assert_called_once_with(
|
||||
"foo",
|
||||
'"bar"',
|
||||
ex=expected_redis_client_ttl,
|
||||
)
|
||||
|
||||
|
||||
def test_raises_if_key_doesnt_match_arguments(cache):
|
||||
@cache.set("{baz}")
|
||||
def foo(bar):
|
||||
pass
|
||||
|
||||
with pytest.raises(KeyError):
|
||||
foo(1)
|
||||
|
||||
with pytest.raises(KeyError):
|
||||
foo()
|
||||
|
||||
|
||||
def test_get(mocker, mocked_redis_client, cache):
|
||||
mock_redis_get = mocker.patch.object(
|
||||
mocked_redis_client,
|
||||
"get",
|
||||
return_value=b'"bar"',
|
||||
)
|
||||
|
||||
@cache.set("{a}-{b}-{c}")
|
||||
def foo(a, b, c):
|
||||
# This function should not be called because the cache has
|
||||
# returned a value
|
||||
raise RuntimeError
|
||||
|
||||
assert foo(1, 2, 3) == "bar"
|
||||
|
||||
mock_redis_get.assert_called_once_with("1-2-3")
|
||||
|
||||
|
||||
def test_delete(mocker, mocked_redis_client, cache):
|
||||
mock_redis_delete = mocker.patch.object(
|
||||
mocked_redis_client,
|
||||
"delete",
|
||||
)
|
||||
|
||||
@cache.delete("{a}-{b}-{c}")
|
||||
def foo(a, b, c):
|
||||
return "bar"
|
||||
|
||||
assert foo(1, 2, 3) == "bar"
|
||||
|
||||
mock_redis_delete.assert_called_once_with("1-2-3")
|
||||
|
||||
|
||||
def test_delete_even_if_call_raises(mocker, mocked_redis_client, cache):
|
||||
mock_redis_delete = mocker.patch.object(
|
||||
mocked_redis_client,
|
||||
"delete",
|
||||
)
|
||||
|
||||
@cache.delete("bar")
|
||||
def foo():
|
||||
raise RuntimeError
|
||||
|
||||
with pytest.raises(RuntimeError):
|
||||
foo()
|
||||
|
||||
mock_redis_delete.assert_called_once_with("bar")
|
||||
|
||||
|
||||
def test_delete_by_pattern(mocker, mocked_redis_client, cache):
|
||||
mock_redis_delete = mocker.patch.object(
|
||||
mocked_redis_client,
|
||||
"delete_by_pattern",
|
||||
)
|
||||
|
||||
@cache.delete_by_pattern("{a}-{b}-{c}-???")
|
||||
def foo(a, b, c):
|
||||
return "bar"
|
||||
|
||||
assert foo(1, 2, 3) == "bar"
|
||||
|
||||
mock_redis_delete.assert_called_once_with("1-2-3-???")
|
||||
|
||||
|
||||
def test_delete_by_pattern_even_if_call_raises(mocker, mocked_redis_client, cache):
|
||||
mock_redis_delete = mocker.patch.object(
|
||||
mocked_redis_client,
|
||||
"delete_by_pattern",
|
||||
)
|
||||
|
||||
@cache.delete_by_pattern("bar-???")
|
||||
def foo():
|
||||
raise RuntimeError
|
||||
|
||||
with pytest.raises(RuntimeError):
|
||||
foo()
|
||||
|
||||
mock_redis_delete.assert_called_once_with("bar-???")
|
||||
Reference in New Issue
Block a user