mirror of
https://github.com/GSA/notifications-api.git
synced 2026-01-30 14:31:57 -05:00
Implemented the rate limiting from Redis
- Uses Redis cache to check for current count - If not present then sets the value based on the database state - Any Redis errors are swallowed. Cache failures should NOT fail the request.
This commit is contained in:
@@ -2,36 +2,68 @@ import pytest
|
||||
from unittest.mock import Mock
|
||||
|
||||
from app.clients.redis.redis_client import RedisClient
|
||||
from app.clients.redis import cache_key
|
||||
from freezegun import freeze_time
|
||||
|
||||
|
||||
@pytest.fixture(scope='function')
|
||||
def enabled_redis_client(notify_api):
|
||||
def enabled_redis_client(notify_api, mocker):
|
||||
notify_api.config['REDIS_ENABLED'] = True
|
||||
|
||||
redis_client = RedisClient()
|
||||
redis_client.init_app(notify_api)
|
||||
redis_client.redis_store = Mock()
|
||||
return redis_client
|
||||
return build_redis_client(notify_api, mocker)
|
||||
|
||||
|
||||
@pytest.fixture(scope='function')
|
||||
def disabled_redis_client(notify_api):
|
||||
def disabled_redis_client(notify_api, mocker):
|
||||
notify_api.config['REDIS_ENABLED'] = False
|
||||
return build_redis_client(notify_api, mocker)
|
||||
|
||||
|
||||
def build_redis_client(notify_api, mocker):
|
||||
redis_client = RedisClient()
|
||||
redis_client.init_app(notify_api)
|
||||
redis_client.redis_store = Mock()
|
||||
mocker.patch.object(redis_client.redis_store, 'get', return_value=100)
|
||||
mocker.patch.object(redis_client.redis_store, 'set')
|
||||
return redis_client
|
||||
|
||||
|
||||
def test_should_not_raise_exception_if_raise_set_to_false(notify_api):
|
||||
notify_api.config['REDIS_ENABLED'] = True
|
||||
redis_client = RedisClient()
|
||||
redis_client.init_app(notify_api)
|
||||
redis_client.redis_store.get = Mock(side_effect=Exception())
|
||||
redis_client.redis_store.set = Mock(side_effect=Exception())
|
||||
redis_client.redis_store.inc = Mock(side_effect=Exception())
|
||||
assert redis_client.get('test') is None
|
||||
assert redis_client.set('test', 'test') is None
|
||||
assert redis_client.inc('test')is None
|
||||
|
||||
|
||||
def test_should_raise_exception_if_raise_set_to_true(notify_api):
|
||||
notify_api.config['REDIS_ENABLED'] = True
|
||||
redis_client = RedisClient()
|
||||
redis_client.init_app(notify_api)
|
||||
redis_client.redis_store.get = Mock(side_effect=Exception('get failed'))
|
||||
redis_client.redis_store.set = Mock(side_effect=Exception('set failed'))
|
||||
redis_client.redis_store.inc = Mock(side_effect=Exception('inc failed'))
|
||||
with pytest.raises(Exception) as e:
|
||||
redis_client.get('test', raise_exception=True)
|
||||
assert str(e.value) == 'get failed'
|
||||
with pytest.raises(Exception) as e:
|
||||
redis_client.set('test', 'test', raise_exception=True)
|
||||
assert str(e.value) == 'set failed'
|
||||
with pytest.raises(Exception) as e:
|
||||
redis_client.inc('test', raise_exception=True)
|
||||
assert str(e.value) == 'inc failed'
|
||||
|
||||
|
||||
def test_should_not_call_set_if_not_enabled(disabled_redis_client):
|
||||
disabled_redis_client.set('key', 'value')
|
||||
assert not disabled_redis_client.set('key', 'value')
|
||||
disabled_redis_client.redis_store.set.assert_not_called()
|
||||
|
||||
|
||||
def test_should_call_set_if_enabled(enabled_redis_client):
|
||||
enabled_redis_client.set('key', 'value')
|
||||
enabled_redis_client.redis_store.set.assert_called_with('key', 'value')
|
||||
enabled_redis_client.redis_store.set.assert_called_with('key', 'value', None, None, False, False)
|
||||
|
||||
|
||||
def test_should_not_call_get_if_not_enabled(disabled_redis_client):
|
||||
@@ -40,5 +72,10 @@ def test_should_not_call_get_if_not_enabled(disabled_redis_client):
|
||||
|
||||
|
||||
def test_should_call_get_if_enabled(enabled_redis_client):
|
||||
enabled_redis_client.get('key')
|
||||
enabled_redis_client.redis_store.get.assert_called_with('key')
|
||||
assert enabled_redis_client.get('key') == 100
|
||||
enabled_redis_client.redis_store.get.assert_called_with('key')
|
||||
|
||||
|
||||
def test_should_build_cache_key_service_and_action(sample_service):
|
||||
with freeze_time("2016-01-01 12:00:00.000000"):
|
||||
assert cache_key(sample_service.id) == '{}-2016-01-01-count'.format(sample_service.id)
|
||||
|
||||
@@ -343,7 +343,7 @@ def test_should_allow_valid_email_notification(notify_api, sample_email_template
|
||||
|
||||
|
||||
@freeze_time("2016-01-01 12:00:00.061258")
|
||||
def test_should_not_block_api_call_if_over_day_limit_for_live_service(
|
||||
def test_should_block_api_call_if_over_day_limit_for_live_service(
|
||||
notify_db,
|
||||
notify_db_session,
|
||||
notify_api,
|
||||
@@ -371,8 +371,7 @@ def test_should_not_block_api_call_if_over_day_limit_for_live_service(
|
||||
data=json.dumps(data),
|
||||
headers=[('Content-Type', 'application/json'), auth_header])
|
||||
json.loads(response.get_data(as_text=True))
|
||||
|
||||
assert response.status_code == 201
|
||||
assert response.status_code == 429
|
||||
|
||||
|
||||
@freeze_time("2016-01-01 12:00:00.061258")
|
||||
|
||||
@@ -1,36 +1,148 @@
|
||||
import pytest
|
||||
from app.notifications.validators import check_service_message_limit, check_template_is_for_notification_type, \
|
||||
check_template_is_active, service_can_send_to_recipient, check_sms_content_char_count
|
||||
from app.v2.errors import BadRequestError, TooManyRequestsError
|
||||
from tests.app.conftest import (sample_notification as create_notification,
|
||||
sample_service as create_service, sample_service_whitelist)
|
||||
from freezegun import freeze_time
|
||||
|
||||
|
||||
@pytest.mark.parametrize('key_type', ['test', 'team', 'normal'])
|
||||
def test_check_service_message_limit_with_unrestricted_service_passes(key_type,
|
||||
sample_service,
|
||||
sample_notification):
|
||||
assert check_service_message_limit(key_type, sample_service) is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize('key_type', ['test', 'team', 'normal'])
|
||||
def test_check_service_message_limit_under_message_limit_passes(key_type,
|
||||
sample_service,
|
||||
sample_notification):
|
||||
assert check_service_message_limit(key_type, sample_service) is None
|
||||
import app
|
||||
from app.notifications.validators import (
|
||||
check_service_message_limit,
|
||||
check_template_is_for_notification_type,
|
||||
check_template_is_active,
|
||||
service_can_send_to_recipient,
|
||||
check_sms_content_char_count
|
||||
)
|
||||
from app.v2.errors import (
|
||||
BadRequestError,
|
||||
TooManyRequestsError
|
||||
)
|
||||
from tests.app.conftest import (
|
||||
sample_notification as create_notification,
|
||||
sample_service as create_service,
|
||||
sample_service_whitelist
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('key_type', ['team', 'normal'])
|
||||
def test_check_service_message_limit_over_message_limit_fails(key_type, notify_db, notify_db_session):
|
||||
def test_exception_thown_by_redis_store_get_should_not_be_fatal(
|
||||
notify_db,
|
||||
notify_db_session,
|
||||
key_type,
|
||||
mocker):
|
||||
mocker.patch('app.notifications.validators.redis_store.redis_store.get', side_effect=Exception("broken redis"))
|
||||
mocker.patch('app.notifications.validators.redis_store.set')
|
||||
|
||||
service = create_service(notify_db, notify_db_session, restricted=True, limit=4)
|
||||
for x in range(5):
|
||||
create_notification(notify_db, notify_db_session, service=service)
|
||||
|
||||
with pytest.raises(TooManyRequestsError) as e:
|
||||
check_service_message_limit(key_type, service)
|
||||
assert e.value.status_code == 429
|
||||
assert e.value.code == '10429'
|
||||
assert e.value.message == 'Exceeded send limits (4) for today'
|
||||
assert e.value.fields == []
|
||||
app.notifications.validators.redis_store.set.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.parametrize('key_type', ['test', 'team', 'normal'])
|
||||
def test_exception_thown_by_redis_store_set_should_not_be_fatal(
|
||||
key_type,
|
||||
sample_service,
|
||||
mocker):
|
||||
mocker.patch('app.notifications.validators.redis_store.redis_store.set', side_effect=Exception("broken redis"))
|
||||
mocker.patch('app.notifications.validators.redis_store.get', return_value=None)
|
||||
assert not check_service_message_limit(key_type, sample_service)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('key_type', ['test', 'team', 'normal'])
|
||||
def test_check_service_message_limit_in_cache_with_unrestricted_service_passes(
|
||||
key_type,
|
||||
sample_service,
|
||||
mocker):
|
||||
mocker.patch('app.notifications.validators.redis_store.get', return_value=1)
|
||||
mocker.patch('app.notifications.validators.redis_store.set')
|
||||
mocker.patch('app.notifications.validators.services_dao')
|
||||
assert not check_service_message_limit(key_type, sample_service)
|
||||
app.notifications.validators.redis_store.set.assert_not_called()
|
||||
assert not app.notifications.validators.services_dao.mock_calls
|
||||
|
||||
|
||||
@pytest.mark.parametrize('key_type', ['test', 'team', 'normal'])
|
||||
def test_check_service_message_limit_in_cache_under_message_limit_passes(
|
||||
key_type,
|
||||
sample_service,
|
||||
mocker):
|
||||
mocker.patch('app.notifications.validators.redis_store.get', return_value=1)
|
||||
mocker.patch('app.notifications.validators.redis_store.set')
|
||||
mocker.patch('app.notifications.validators.services_dao')
|
||||
assert not check_service_message_limit(key_type, sample_service)
|
||||
app.notifications.validators.redis_store.set.assert_not_called()
|
||||
assert not app.notifications.validators.services_dao.mock_calls
|
||||
|
||||
|
||||
def test_should_not_interact_with_cache_for_test_key(sample_service, mocker):
|
||||
mocker.patch('app.notifications.validators.redis_store')
|
||||
assert not check_service_message_limit('test', sample_service)
|
||||
assert not app.notifications.validators.redis_store.mock_calls
|
||||
|
||||
|
||||
@pytest.mark.parametrize('key_type', ['team', 'normal'])
|
||||
def test_should_set_cache_value_as_value_from_database_if_cache_not_set(
|
||||
key_type,
|
||||
notify_db,
|
||||
notify_db_session,
|
||||
sample_service,
|
||||
mocker
|
||||
):
|
||||
with freeze_time("2016-01-01 12:00:00.000000"):
|
||||
for x in range(5):
|
||||
create_notification(notify_db, notify_db_session, service=sample_service)
|
||||
mocker.patch('app.notifications.validators.redis_store.get', return_value=None)
|
||||
mocker.patch('app.notifications.validators.redis_store.set')
|
||||
assert not check_service_message_limit(key_type, sample_service)
|
||||
app.notifications.validators.redis_store.set.assert_called_with(
|
||||
str(sample_service.id) + "-2016-01-01-count", 5, ex=3600
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('key_type', ['team', 'normal'])
|
||||
def test_check_service_message_limit_over_message_limit_fails(key_type, notify_db, notify_db_session, mocker):
|
||||
with freeze_time("2016-01-01 12:00:00.000000"):
|
||||
mocker.patch('app.redis_store.get', return_value=None)
|
||||
mocker.patch('app.notifications.validators.redis_store.set')
|
||||
|
||||
service = create_service(notify_db, notify_db_session, restricted=True, limit=4)
|
||||
for x in range(5):
|
||||
create_notification(notify_db, notify_db_session, service=service)
|
||||
with pytest.raises(TooManyRequestsError) as e:
|
||||
check_service_message_limit(key_type, service)
|
||||
assert e.value.status_code == 429
|
||||
assert e.value.code == '10429'
|
||||
assert e.value.message == 'Exceeded send limits (4) for today'
|
||||
assert e.value.fields == []
|
||||
app.notifications.validators.redis_store.set.assert_called_with(
|
||||
str(service.id) + "-2016-01-01-count", 5, ex=3600
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('key_type', ['team', 'normal'])
|
||||
def test_check_service_message_limit_in_cache_over_message_limit_fails(
|
||||
notify_db,
|
||||
notify_db_session,
|
||||
key_type,
|
||||
mocker):
|
||||
with freeze_time("2016-01-01 12:00:00.000000"):
|
||||
mocker.patch('app.redis_store.get', return_value=5)
|
||||
mocker.patch('app.notifications.validators.redis_store.set')
|
||||
mocker.patch('app.notifications.validators.services_dao')
|
||||
|
||||
service = create_service(notify_db, notify_db_session, restricted=True, limit=4)
|
||||
with pytest.raises(TooManyRequestsError) as e:
|
||||
check_service_message_limit(key_type, service)
|
||||
assert e.value.status_code == 429
|
||||
assert e.value.code == '10429'
|
||||
assert e.value.message == 'Exceeded send limits (4) for today'
|
||||
assert e.value.fields == []
|
||||
app.notifications.validators.redis_store.set.assert_not_called()
|
||||
assert not app.notifications.validators.services_dao.mock_calls
|
||||
|
||||
|
||||
@pytest.mark.parametrize('template_type, notification_type',
|
||||
|
||||
Reference in New Issue
Block a user