339 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Python
		
	
	
	
			
		
		
	
	
			339 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Python
		
	
	
	
| import time
 | |
| 
 | |
| import pytest
 | |
| 
 | |
| from funkwhale_api.common import throttling
 | |
| 
 | |
| 
 | |
| def test_get_ident_anonymous(api_request):
 | |
|     ip = "92.92.92.92"
 | |
|     request = api_request.get("/", HTTP_X_FORWARDED_FOR=ip)
 | |
| 
 | |
|     expected = {"id": ip, "type": "anonymous"}
 | |
| 
 | |
|     assert throttling.get_ident(None, request) == expected
 | |
| 
 | |
| 
 | |
| def test_get_ident_authenticated(api_request, factories):
 | |
|     user = factories["users.User"]()
 | |
|     request = api_request.get("/")
 | |
|     expected = {"id": f"{user.pk}", "type": "authenticated"}
 | |
|     assert throttling.get_ident(user, request) == expected
 | |
| 
 | |
| 
 | |
| @pytest.mark.parametrize(
 | |
|     "scope, ident, expected",
 | |
|     [
 | |
|         (
 | |
|             "create",
 | |
|             {"id": "42", "type": "authenticated"},
 | |
|             "throttling:create:authenticated:42",
 | |
|         ),
 | |
|         (
 | |
|             "list",
 | |
|             {"id": "92.92.92.92", "type": "anonymous"},
 | |
|             "throttling:list:anonymous:92.92.92.92",
 | |
|         ),
 | |
|     ],
 | |
| )
 | |
| def test_get_cache_key(scope, ident, expected):
 | |
|     assert throttling.get_cache_key(scope, ident) == expected
 | |
| 
 | |
| 
 | |
| @pytest.mark.parametrize(
 | |
|     "action, type, view_conf, throttling_actions, expected",
 | |
|     [
 | |
|         # exact match, we return the rate
 | |
|         ("retrieve", "anonymous", {}, {"retrieve": {"anonymous": "test"}}, "test"),
 | |
|         # exact match on the view, we return the rate
 | |
|         ("retrieve", "anonymous", {"retrieve": {"anonymous": "test"}}, {}, "test"),
 | |
|         # no match, we return nothing
 | |
|         ("retrieve", "authenticated", {}, {}, None),
 | |
|         ("retrieve", "authenticated", {}, {"retrieve": {"anonymous": "test"}}, None),
 | |
|         (
 | |
|             "retrieve",
 | |
|             "authenticated",
 | |
|             {"destroy": {"authenticated": "test"}},
 | |
|             {"retrieve": {"anonymous": "test"}},
 | |
|             None,
 | |
|         ),
 | |
|         # exact match on the view, and in the settings, the view is more important
 | |
|         (
 | |
|             "retrieve",
 | |
|             "anonymous",
 | |
|             {"retrieve": {"anonymous": "test"}},
 | |
|             {"retrieve": {"anonymous": "test-2"}},
 | |
|             "test",
 | |
|         ),
 | |
|         # wildcard match, we return the wildcard value
 | |
|         ("retrieve", "authenticated", {}, {"*": {"authenticated": "test"}}, "test"),
 | |
|         # wildcard match, but more specific match also, we use this one instead
 | |
|         (
 | |
|             "retrieve",
 | |
|             "authenticated",
 | |
|             {},
 | |
|             {"retrieve": {"authenticated": "test-2"}, "*": {"authenticated": "test"}},
 | |
|             "test-2",
 | |
|         ),
 | |
|     ],
 | |
| )
 | |
| def test_get_rate_for_scope_and_ident_type(
 | |
|     action, type, view_conf, throttling_actions, expected, settings
 | |
| ):
 | |
|     settings.THROTTLING_SCOPES = throttling_actions
 | |
|     assert (
 | |
|         throttling.get_scope_for_action_and_ident_type(
 | |
|             action=action, ident_type=type, view_conf=view_conf
 | |
|         )
 | |
|         is expected
 | |
|     )
 | |
| 
 | |
| 
 | |
| @pytest.mark.parametrize(
 | |
|     "view_args, throttling_rates, previous_requests, expected",
 | |
|     [
 | |
|         # room for one more requests
 | |
|         (
 | |
|             {
 | |
|                 "action": "retrieve",
 | |
|                 "throttling_scopes": {"retrieve": {"anonymous": "test"}},
 | |
|             },
 | |
|             {"test": {"rate": "3/s"}},
 | |
|             2,
 | |
|             True,
 | |
|         ),
 | |
|         # number of requests exceeded
 | |
|         (
 | |
|             {
 | |
|                 "action": "retrieve",
 | |
|                 "throttling_scopes": {"retrieve": {"anonymous": "test"}},
 | |
|             },
 | |
|             {"test": {"rate": "3/s"}},
 | |
|             3,
 | |
|             False,
 | |
|         ),
 | |
|         # no throttling setup
 | |
|         (
 | |
|             {
 | |
|                 "action": "delete",
 | |
|                 "throttling_scopes": {"retrieve": {"anonymous": "test"}},
 | |
|             },
 | |
|             {},
 | |
|             1000,
 | |
|             True,
 | |
|         ),
 | |
|     ],
 | |
| )
 | |
| def test_throttle_anonymous(
 | |
|     view_args,
 | |
|     throttling_rates,
 | |
|     previous_requests,
 | |
|     expected,
 | |
|     api_request,
 | |
|     mocker,
 | |
|     settings,
 | |
| ):
 | |
|     settings.THROTTLING_RATES = throttling_rates
 | |
|     settings.THROTTLING_SCOPES = {}
 | |
|     ip = "92.92.92.92"
 | |
|     ident = {"type": "anonymous", "id": ip}
 | |
|     request = api_request.get("/", HTTP_X_FORWARDED_FOR=ip)
 | |
| 
 | |
|     view = mocker.Mock(**view_args)
 | |
| 
 | |
|     cache_key = throttling.get_cache_key("test", ident)
 | |
|     throttle = throttling.FunkwhaleThrottle()
 | |
| 
 | |
|     history = [time.time() for _ in range(previous_requests)]
 | |
|     throttle.cache.set(cache_key, history)
 | |
| 
 | |
|     assert throttle.allow_request(request, view) is expected
 | |
| 
 | |
| 
 | |
| @pytest.mark.parametrize(
 | |
|     "view_args, throttling_rates, previous_requests, expected",
 | |
|     [
 | |
|         # room for one more requests
 | |
|         (
 | |
|             {
 | |
|                 "action": "retrieve",
 | |
|                 "throttling_scopes": {"retrieve": {"authenticated": "test"}},
 | |
|             },
 | |
|             {"test": {"rate": "3/s"}},
 | |
|             2,
 | |
|             True,
 | |
|         ),
 | |
|         # number of requests exceeded
 | |
|         (
 | |
|             {
 | |
|                 "action": "retrieve",
 | |
|                 "throttling_scopes": {"retrieve": {"authenticated": "test"}},
 | |
|             },
 | |
|             {"test": {"rate": "3/s"}},
 | |
|             3,
 | |
|             False,
 | |
|         ),
 | |
|         # no throttling setup
 | |
|         (
 | |
|             {
 | |
|                 "action": "delete",
 | |
|                 "throttling_scopes": {"retrieve": {"authenticated": "test"}},
 | |
|             },
 | |
|             {},
 | |
|             1000,
 | |
|             True,
 | |
|         ),
 | |
|     ],
 | |
| )
 | |
| def test_throttle_authenticated(
 | |
|     view_args,
 | |
|     throttling_rates,
 | |
|     previous_requests,
 | |
|     expected,
 | |
|     api_request,
 | |
|     mocker,
 | |
|     settings,
 | |
|     factories,
 | |
| ):
 | |
|     settings.THROTTLING_RATES = throttling_rates
 | |
|     settings.THROTTLING_SCOPES = {}
 | |
|     user = factories["users.User"]()
 | |
|     ident = {"type": "authenticated", "id": user.pk}
 | |
|     request = api_request.get("/")
 | |
|     setattr(request, "user", user)
 | |
| 
 | |
|     view = mocker.Mock(**view_args)
 | |
| 
 | |
|     cache_key = throttling.get_cache_key("test", ident)
 | |
|     throttle = throttling.FunkwhaleThrottle()
 | |
| 
 | |
|     history = [time.time() for _ in range(previous_requests)]
 | |
|     throttle.cache.set(cache_key, history)
 | |
| 
 | |
|     assert throttle.allow_request(request, view) is expected
 | |
| 
 | |
| 
 | |
| def throttle_successive(settings, mocker, api_request):
 | |
|     settings.THROTTLING_RATES = {"test": {"rate": "3/s"}}
 | |
|     settings.THROTTLING_SCOPES = {}
 | |
|     ip = "92.92.92.92"
 | |
|     request = api_request.get("/", HTTP_X_FORWARDED_FOR=ip)
 | |
| 
 | |
|     view = mocker.Mock(
 | |
|         action="retrieve", throttling_scopes={"retrieve": {"anonymous": "test"}}
 | |
|     )
 | |
| 
 | |
|     throttle = throttling.FunkwhaleThrottle()
 | |
| 
 | |
|     assert throttle.allow_request(request, view) is True
 | |
|     assert throttle.allow_request(request, view) is True
 | |
|     assert throttle.allow_request(request, view) is True
 | |
|     assert throttle.allow_request(request, view) is False
 | |
| 
 | |
| 
 | |
| def test_throttle_attach_info(mocker):
 | |
|     throttle = throttling.FunkwhaleThrottle()
 | |
|     request = mocker.Mock()
 | |
|     setattr(throttle, "num_requests", 300)
 | |
|     setattr(throttle, "duration", 3600)
 | |
|     setattr(throttle, "scope", "hello")
 | |
|     setattr(throttle, "history", [])
 | |
|     setattr(throttle, "request", request)
 | |
| 
 | |
|     expected = {
 | |
|         "num_requests": throttle.num_requests,
 | |
|         "duration": throttle.duration,
 | |
|         "history": throttle.history,
 | |
|         "wait": throttle.wait(),
 | |
|         "scope": throttle.scope,
 | |
|     }
 | |
|     throttle.attach_info()
 | |
| 
 | |
|     assert request._throttle_status == expected
 | |
| 
 | |
| 
 | |
| @pytest.mark.parametrize("method", ["throttle_success", "throttle_failure"])
 | |
| def test_throttle_calls_attach_info(method, mocker):
 | |
|     throttle = throttling.FunkwhaleThrottle()
 | |
|     setattr(throttle, "key", "noop")
 | |
|     setattr(throttle, "now", "noop")
 | |
|     setattr(throttle, "duration", "noop")
 | |
|     setattr(throttle, "history", ["noop"])
 | |
|     mocker.patch.object(throttle, "cache")
 | |
|     attach_info = mocker.patch.object(throttle, "attach_info")
 | |
|     func = getattr(throttle, method)
 | |
| 
 | |
|     func()
 | |
| 
 | |
|     attach_info.assert_called_once_with()
 | |
| 
 | |
| 
 | |
| def test_allow_request(api_request, settings, mocker):
 | |
|     settings.THROTTLING_ENABLED = True
 | |
|     settings.THROTTLING_RATES = {"test": {"rate": "2/s"}}
 | |
|     ip = "92.92.92.92"
 | |
|     request = api_request.get("/", HTTP_X_FORWARDED_FOR=ip)
 | |
|     allow_request = mocker.spy(throttling.FunkwhaleThrottle, "allow_request")
 | |
|     action = "test"
 | |
|     throttling_scopes = {"test": {"anonymous": "test", "authenticated": "test"}}
 | |
|     throttling.check_request(request, action)
 | |
|     throttling.check_request(request, action)
 | |
|     with pytest.raises(throttling.TooManyRequests):
 | |
|         throttling.check_request(request, action)
 | |
| 
 | |
|     assert allow_request.call_count == 3
 | |
|     assert allow_request.call_args[0][1] == request
 | |
|     assert allow_request.call_args[0][2] == throttling.DummyView(
 | |
|         action=action, throttling_scopes=throttling_scopes
 | |
|     )
 | |
| 
 | |
| 
 | |
| def test_allow_request_throttling_disabled(api_request, settings):
 | |
|     settings.THROTTLING_RATES = {"test": {"rate": "1/s"}}
 | |
|     settings.THROTTLING_ENABLED = False
 | |
|     ip = "92.92.92.92"
 | |
|     request = api_request.get("/", HTTP_X_FORWARDED_FOR=ip)
 | |
|     action = "test"
 | |
|     throttling.check_request(request, action)
 | |
|     # even exceeding request doesn't raise any exception
 | |
|     throttling.check_request(request, action)
 | |
| 
 | |
| 
 | |
| def test_get_throttling_status_for_ident(settings, cache):
 | |
|     settings.THROTTLING_RATES = {
 | |
|         "test-1": {"rate": "30/d", "description": "description 1"},
 | |
|         "test-2": {"rate": "20/h", "description": "description 2"},
 | |
|     }
 | |
|     ident = {"type": "anonymous", "id": "92.92.92.92"}
 | |
|     test1_cache_key = throttling.get_cache_key("test-1", ident)
 | |
|     now = int(time.time())
 | |
|     cache.set(test1_cache_key, [now - 1, now - 2, now - 99999999])
 | |
| 
 | |
|     expected = [
 | |
|         {
 | |
|             "id": "test-1",
 | |
|             "limit": 30,
 | |
|             "rate": "30/d",
 | |
|             "description": "description 1",
 | |
|             "duration": 24 * 3600,
 | |
|             "remaining": 28,
 | |
|             "reset": now + (24 * 3600) - 1,
 | |
|             "reset_seconds": (24 * 3600) - 1,
 | |
|             "available": None,
 | |
|             "available_seconds": None,
 | |
|         },
 | |
|         {
 | |
|             "id": "test-2",
 | |
|             "limit": 20,
 | |
|             "rate": "20/h",
 | |
|             "description": "description 2",
 | |
|             "duration": 3600,
 | |
|             "remaining": 20,
 | |
|             "reset": None,
 | |
|             "reset_seconds": None,
 | |
|             "available": None,
 | |
|             "available_seconds": None,
 | |
|         },
 | |
|     ]
 | |
|     assert throttling.get_status(ident, now) == expected
 |