Localize notification_utils to the admin

This changeset pulls in all of the notification_utils code directly into the admin and removes it as an external dependency. We are doing this to cut down on operational maintenance of the project and will begin removing parts of it no longer needed for the admin.

Signed-off-by: Carlo Costino <carlo.costino@gsa.gov>
This commit is contained in:
Carlo Costino
2024-05-16 10:37:37 -04:00
parent 6bab682edd
commit 9a83ba7475
122 changed files with 49925 additions and 268 deletions

View 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"])

View 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-???")