diff --git a/api/environments/authentication.py b/api/environments/authentication.py index 5be62105f907..8fc7d1e0e12b 100644 --- a/api/environments/authentication.py +++ b/api/environments/authentication.py @@ -1,7 +1,6 @@ from common.gunicorn.utils import log_extra from django.conf import settings from django.core.cache import caches -from django.http import HttpRequest from rest_framework.authentication import BaseAuthentication from rest_framework.exceptions import AuthenticationFailed @@ -48,19 +47,3 @@ def authenticate(self, request): # type: ignore[no-untyped-def] # DRF authentication expects a two tuple to be returned containing User, auth return None, None - - -class AuthenticatedRequest(HttpRequest): - _environment: Environment - - @property - def environment(self) -> Environment: - if not self._environment: - raise AssertionError( - "Tried to access environment on an authenticated request, but was None" - ) - return self._environment - - @environment.setter - def environment(self, environment: Environment) -> None: - self._environment = environment diff --git a/api/environments/models.py b/api/environments/models.py index d872645c6d14..17eb27999ca3 100644 --- a/api/environments/models.py +++ b/api/environments/models.py @@ -165,7 +165,9 @@ def create_feature_states(self) -> None: @hook(AFTER_UPDATE) # type: ignore[misc] def clear_environment_cache(self) -> None: # TODO: this could rebuild the cache itself (using an async task) - environment_cache.delete(self.initial_value("api_key")) + environment_cache.delete_many( + [self.initial_value("api_key"), *[eak.key for eak in self.api_keys.all()]] + ) @hook(AFTER_UPDATE, when="api_key", has_changed=True) # type: ignore[misc] def update_environment_document_cache(self) -> None: diff --git a/api/environments/sdk/views.py b/api/environments/sdk/views.py index 58248d53b12b..ad73b8e3d45f 100644 --- a/api/environments/sdk/views.py +++ b/api/environments/sdk/views.py @@ -4,12 +4,12 @@ from django.utils.decorators import method_decorator from django.views.decorators.http import condition from drf_yasg.utils import swagger_auto_schema # type: ignore[import-untyped] +from rest_framework.request import Request from rest_framework.response import Response from rest_framework.views import APIView from core.constants import FLAGSMITH_UPDATED_AT_HEADER from environments.authentication import ( - AuthenticatedRequest, EnvironmentKeyAuthentication, ) from environments.models import Environment @@ -17,7 +17,7 @@ from environments.sdk.schemas import SDKEnvironmentDocumentModel -def get_last_modified(request: AuthenticatedRequest) -> datetime | None: +def get_last_modified(request: Request) -> datetime | None: updated_at: Optional[datetime] = request.environment.updated_at return updated_at @@ -31,7 +31,7 @@ def get_authenticators(self): # type: ignore[no-untyped-def] @swagger_auto_schema(responses={200: SDKEnvironmentDocumentModel}) # type: ignore[misc] @method_decorator(condition(last_modified_func=get_last_modified)) - def get(self, request: AuthenticatedRequest) -> Response: + def get(self, request: Request) -> Response: environment_document = Environment.get_environment_document( request.environment.api_key, ) diff --git a/api/tests/unit/environments/test_unit_environments_models.py b/api/tests/unit/environments/test_unit_environments_models.py index 55c068fcb621..0aa5abb336cc 100644 --- a/api/tests/unit/environments/test_unit_environments_models.py +++ b/api/tests/unit/environments/test_unit_environments_models.py @@ -408,9 +408,9 @@ def test_save_environment_clears_environment_cache(mocker, project): # type: ig environment.save() # Then - mock_calls = mock_environment_cache.delete.mock_calls + mock_calls = mock_environment_cache.delete_many.mock_calls assert len(mock_calls) == 2 - assert mock_calls[0][1][0] == mock_calls[1][1][0] == old_key + assert mock_calls[0][1][0] == mock_calls[1][1][0] == [old_key] @pytest.mark.parametrize( diff --git a/api/tests/unit/environments/test_unit_environments_views_sdk_environment.py b/api/tests/unit/environments/test_unit_environments_views_sdk_environment.py index 9992e28dec1c..949bde16a782 100644 --- a/api/tests/unit/environments/test_unit_environments_views_sdk_environment.py +++ b/api/tests/unit/environments/test_unit_environments_views_sdk_environment.py @@ -1,7 +1,9 @@ +import time from typing import TYPE_CHECKING import pytest from django.urls import reverse +from django.utils import timezone from django.utils.http import http_date from flag_engine.segments.constants import EQUAL from rest_framework import status @@ -196,23 +198,25 @@ def test_environment_document_caching( # Then - first request should return 200 and include Last-Modified header assert response1.status_code == status.HTTP_200_OK - assert response1.headers["Last-Modified"] == http_date( - environment.updated_at.timestamp() - ) + last_modified = response1.headers["Last-Modified"] + assert last_modified == http_date(environment.updated_at.timestamp()) # When - second request with If-Modified-Since header client.credentials( HTTP_X_ENVIRONMENT_KEY=api_key, - HTTP_IF_MODIFIED_SINCE=http_date(environment.updated_at.timestamp()), + HTTP_IF_MODIFIED_SINCE=last_modified, ) response2 = client.get(url) # Then - second request should return 304 Not Modified assert response2.status_code == status.HTTP_304_NOT_MODIFIED + # sleep for 1s since If-Modified-Since is only accurate to the nearest second + # https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/If-Modified-Since + time.sleep(1) + # When - environment is updated - environment.clear_environment_cache() - environment.name = "Updated" + environment.updated_at = timezone.now() environment.save() # Then - request with same If-Modified-Since header should return 200 diff --git a/api/tests/unit/integrations/amplitude/test_unit_amplitude_models.py b/api/tests/unit/integrations/amplitude/test_unit_amplitude_models.py index 86c6214ab42b..e858319e4a0a 100644 --- a/api/tests/unit/integrations/amplitude/test_unit_amplitude_models.py +++ b/api/tests/unit/integrations/amplitude/test_unit_amplitude_models.py @@ -60,4 +60,4 @@ def test_amplitude_configuration_update_clears_environment_cache(environment, mo amplitude_config.save() # Then - mock_environment_cache.delete.assert_called_once_with(environment.api_key) + mock_environment_cache.delete_many.assert_called_once_with([environment.api_key])