Skip to content

Commit 8e8ff22

Browse files
feat: If-Modified-Since for /environment-document (#5283)
Co-authored-by: Matthew Elwell <matthew.elwell@flagsmith.com>
1 parent a41b878 commit 8e8ff22

5 files changed

Lines changed: 85 additions & 8 deletions

File tree

api/environments/models.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,9 @@ def create_feature_states(self) -> None:
165165
@hook(AFTER_UPDATE) # type: ignore[misc]
166166
def clear_environment_cache(self) -> None:
167167
# TODO: this could rebuild the cache itself (using an async task)
168-
environment_cache.delete(self.initial_value("api_key"))
168+
environment_cache.delete_many(
169+
[self.initial_value("api_key"), *[eak.key for eak in self.api_keys.all()]]
170+
)
169171

170172
@hook(AFTER_UPDATE, when="api_key", has_changed=True) # type: ignore[misc]
171173
def update_environment_document_cache(self) -> None:

api/environments/sdk/views.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,27 @@
1-
from django.http import HttpRequest
1+
from datetime import datetime
2+
from typing import Optional
3+
4+
from django.utils.decorators import method_decorator
5+
from django.views.decorators.http import condition
26
from drf_yasg.utils import swagger_auto_schema # type: ignore[import-untyped]
7+
from rest_framework.request import Request
38
from rest_framework.response import Response
49
from rest_framework.views import APIView
510

611
from core.constants import FLAGSMITH_UPDATED_AT_HEADER
7-
from environments.authentication import EnvironmentKeyAuthentication
12+
from environments.authentication import (
13+
EnvironmentKeyAuthentication,
14+
)
815
from environments.models import Environment
916
from environments.permissions.permissions import EnvironmentKeyPermissions
1017
from environments.sdk.schemas import SDKEnvironmentDocumentModel
1118

1219

20+
def get_last_modified(request: Request) -> datetime | None:
21+
updated_at: Optional[datetime] = request.environment.updated_at
22+
return updated_at
23+
24+
1325
class SDKEnvironmentAPIView(APIView):
1426
permission_classes = (EnvironmentKeyPermissions,)
1527
throttle_classes = []
@@ -18,9 +30,10 @@ def get_authenticators(self): # type: ignore[no-untyped-def]
1830
return [EnvironmentKeyAuthentication(required_key_prefix="ser.")]
1931

2032
@swagger_auto_schema(responses={200: SDKEnvironmentDocumentModel}) # type: ignore[misc]
21-
def get(self, request: HttpRequest) -> Response:
33+
@method_decorator(condition(last_modified_func=get_last_modified))
34+
def get(self, request: Request) -> Response:
2235
environment_document = Environment.get_environment_document(
23-
request.environment.api_key # type: ignore[attr-defined]
36+
request.environment.api_key,
2437
)
2538
updated_at = self.request.environment.updated_at
2639
return Response(

api/tests/unit/environments/test_unit_environments_models.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -408,9 +408,9 @@ def test_save_environment_clears_environment_cache(mocker, project): # type: ig
408408
environment.save()
409409

410410
# Then
411-
mock_calls = mock_environment_cache.delete.mock_calls
411+
mock_calls = mock_environment_cache.delete_many.mock_calls
412412
assert len(mock_calls) == 2
413-
assert mock_calls[0][1][0] == mock_calls[1][1][0] == old_key
413+
assert mock_calls[0][1][0] == mock_calls[1][1][0] == [old_key]
414414

415415

416416
@pytest.mark.parametrize(

api/tests/unit/environments/test_unit_environments_views_sdk_environment.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1+
import time
12
from typing import TYPE_CHECKING
23

34
import pytest
45
from django.urls import reverse
6+
from django.utils import timezone
7+
from django.utils.http import http_date
58
from flag_engine.segments.constants import EQUAL
69
from rest_framework import status
710
from rest_framework.test import APIClient
@@ -175,3 +178,62 @@ def test_get_environment_document_fails_with_invalid_key(
175178
# We get a 403 since only the server side API keys are able to access the
176179
# environment document
177180
assert response.status_code == status.HTTP_403_FORBIDDEN
181+
182+
183+
def test_environment_document_if_modified_since(
184+
organisation_one: "Organisation",
185+
organisation_one_project_one: "Project",
186+
) -> None:
187+
# Given
188+
project = organisation_one_project_one
189+
environment = Environment.objects.create(name="Test Environment", project=project)
190+
api_key = EnvironmentAPIKey.objects.create(environment=environment).key
191+
192+
client = APIClient()
193+
client.credentials(HTTP_X_ENVIRONMENT_KEY=api_key)
194+
url = reverse("api-v1:environment-document")
195+
196+
# When - first request
197+
response1 = client.get(url)
198+
199+
# Then - first request should return 200 and include Last-Modified header
200+
assert response1.status_code == status.HTTP_200_OK
201+
last_modified = response1.headers["Last-Modified"]
202+
assert last_modified == http_date(environment.updated_at.timestamp())
203+
204+
# When - second request with If-Modified-Since header
205+
client.credentials(
206+
HTTP_X_ENVIRONMENT_KEY=api_key,
207+
HTTP_IF_MODIFIED_SINCE=last_modified,
208+
)
209+
response2 = client.get(url)
210+
211+
# Then - second request should return 304 Not Modified
212+
assert response2.status_code == status.HTTP_304_NOT_MODIFIED
213+
assert len(response2.content) == 0
214+
215+
# sleep for 1s since If-Modified-Since is only accurate to the nearest second
216+
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/If-Modified-Since
217+
time.sleep(1)
218+
219+
# When - environment is updated
220+
environment.updated_at = timezone.now()
221+
environment.save()
222+
223+
# Then - request with same If-Modified-Since header should return 200
224+
response3 = client.get(url)
225+
assert response3.status_code == status.HTTP_200_OK
226+
assert response3.headers["Last-Modified"] == http_date(
227+
environment.updated_at.timestamp()
228+
)
229+
230+
# When - request without If-Modified-Since header
231+
client.credentials(
232+
HTTP_X_ENVIRONMENT_KEY=api_key,
233+
HTTP_IF_MODIFIED_SINCE="",
234+
)
235+
response4 = client.get(url)
236+
237+
# Then - actual environment is returned with a 200
238+
assert response4.status_code == status.HTTP_200_OK
239+
assert len(response4.content) > 0

api/tests/unit/integrations/amplitude/test_unit_amplitude_models.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,4 +60,4 @@ def test_amplitude_configuration_update_clears_environment_cache(environment, mo
6060
amplitude_config.save()
6161

6262
# Then
63-
mock_environment_cache.delete.assert_called_once_with(environment.api_key)
63+
mock_environment_cache.delete_many.assert_called_once_with([environment.api_key])

0 commit comments

Comments
 (0)