diff --git a/ak/tests/test_default_pages.py b/ak/tests/test_default_pages.py index 98722d5a..6433a4ec 100644 --- a/ak/tests/test_default_pages.py +++ b/ak/tests/test_default_pages.py @@ -1,6 +1,8 @@ import pytest import random +from django.test.utils import override_settings + def test_homepage(db, tp): """Ensure we can hit the homepage""" @@ -27,8 +29,28 @@ def test_403_page(db, tp): tp.response_403(response) +@override_settings( + CACHES={ + "default": {"BACKEND": "django.core.cache.backends.locmem.LocMemCache"}, + "machina_attachments": { + "BACKEND": "django.core.cache.backends.locmem.LocMemCache" + }, + "static_content": { + "BACKEND": "django.core.cache.backends.locmem.LocMemCache", + "TIMEOUT": 86400, + }, + } +) def test_404_page(db, tp): - """Test a 404 error page""" + """ + Test a 404 error page + + This test is a bit more complicated than the others because the + this/should/not/exist URL will hit StaticContentTemplateView first + to see if there is static content to serve, and cache it if so. To avoid + errors in CI, we need to make sure that the cache is cleared before + running this test. + """ rando = random.randint(1000, 20000) url = f"/this/should/not/exist/{rando}/" diff --git a/config/settings.py b/config/settings.py index 5335a80f..16a60432 100755 --- a/config/settings.py +++ b/config/settings.py @@ -267,8 +267,16 @@ CACHES = { "BACKEND": "django_redis.cache.RedisCache", "LOCATION": f"redis://{REDIS_HOST}:6379", }, + "static_content": { + "BACKEND": "django_redis.cache.RedisCache", + "LOCATION": f"redis://{REDIS_HOST}:6379/2", + "TIMEOUT": env( + "STATIC_CACHE_TIMEOUT", default=86400 + ), # Cache timeout in seconds: 1 day + }, } + HAYSTACK_CONNECTIONS = { "default": { "ENGINE": "haystack.backends.simple_backend.SimpleEngine", diff --git a/core/tests/test_views.py b/core/tests/test_views.py index 9e82219f..cfde8015 100644 --- a/core/tests/test_views.py +++ b/core/tests/test_views.py @@ -1,11 +1,89 @@ import pytest +import time from unittest.mock import patch +from django.core.cache import caches from django.test import RequestFactory +from django.test.utils import override_settings from django.urls import reverse from core.views import StaticContentTemplateView + +@pytest.fixture +def request_factory(): + """Returns a RequestFactory instance.""" + return RequestFactory() + + +@pytest.fixture +def content_path(): + """Returns a sample content path.""" + return "/some/content/path" + + +def call_view(request_factory, content_path): + """Calls the view with the given request_factory and content path.""" + request = request_factory.get(content_path) + view = StaticContentTemplateView.as_view() + response = view(request, content_path=content_path) + return response + + +@pytest.mark.django_db +@override_settings( + CACHES={ + "default": {"BACKEND": "django.core.cache.backends.locmem.LocMemCache"}, + "machina_attachments": { + "BACKEND": "django.core.cache.backends.locmem.LocMemCache" + }, + "static_content": { + "BACKEND": "django.core.cache.backends.locmem.LocMemCache", + "TIMEOUT": 86400, + }, + } +) +def test_cache_behavior(request_factory, content_path): + """Tests the cache behavior of the StaticContentTemplateView view.""" + # Set up the mocked API call + mock_content = "mocked content" + mock_content_type = "text/plain" + + # Clear the cache before testing + cache = caches["static_content"] + cache.clear() + + with patch("core.views.get_content_from_s3") as mock_get_content_from_s3: + mock_get_content_from_s3.return_value = (mock_content, mock_content_type) + + # Cache miss scenario + response = call_view(request_factory, content_path) + assert response.status_code == 200 + assert response.content.decode() == mock_content + assert response["Content-Type"] == mock_content_type + mock_get_content_from_s3.assert_called_once_with(key=content_path) + + # Cache hit scenario + mock_get_content_from_s3.reset_mock() + response = call_view(request_factory, content_path) + assert response.status_code == 200 + assert response.content.decode() == mock_content + assert response["Content-Type"] == mock_content_type + mock_get_content_from_s3.assert_not_called() + + # Cache expiration scenario + cache.set( + "static_content_" + content_path, (mock_content, mock_content_type), 1 + ) # Set a 1-second cache timeout + time.sleep(2) # Wait for the cache to expire + mock_get_content_from_s3.reset_mock() + response = call_view(request_factory, content_path) + assert response.status_code == 200 + assert response.content.decode() == mock_content + assert response["Content-Type"] == mock_content_type + mock_get_content_from_s3.assert_called_once_with(key=content_path) + + # Define test cases with the paths based on the provided config file static_content_test_cases = [ "/develop/libs/rst.css", # Test a site_path from the config file @@ -26,6 +104,19 @@ def mock_get_content_from_s3(key=None, bucket_name=None): return content_mapping.get(key, None) +@pytest.mark.django_db +@override_settings( + CACHES={ + "default": {"BACKEND": "django.core.cache.backends.locmem.LocMemCache"}, + "machina_attachments": { + "BACKEND": "django.core.cache.backends.locmem.LocMemCache" + }, + "static_content": { + "BACKEND": "django.core.cache.backends.locmem.LocMemCache", + "TIMEOUT": 86400, + }, + } +) @pytest.mark.django_db @pytest.mark.parametrize("content_path", static_content_test_cases) @patch("core.views.get_content_from_s3", new=mock_get_content_from_s3) diff --git a/core/views.py b/core/views.py index 8e5cf317..dd06c550 100644 --- a/core/views.py +++ b/core/views.py @@ -3,6 +3,7 @@ import re import structlog from django.conf import settings +from django.core.cache import caches from django.http import Http404, HttpResponse, HttpResponseNotFound from django.template.response import TemplateResponse from django.views.generic import TemplateView, View @@ -96,16 +97,31 @@ class StaticContentTemplateView(View): Verifies the file and returns the raw static content from S3 mangling paths using the stage_static_config.json settings """ - result = get_content_from_s3(key=kwargs.get("content_path")) - if not result: - logger.info( - "get_content_from_s3_view_no_valid_object", - key=kwargs.get("content_path"), - status_code=404, - ) - return HttpResponseNotFound("Page not found") + content_path = kwargs.get("content_path") - content, content_type = result + # Get the static content cache + static_content_cache = caches["static_content"] + + # Check if the content is in the cache + cache_key = f"static_content_{content_path}" + cached_result = static_content_cache.get(cache_key) + + if cached_result: + content, content_type = cached_result + else: + # Fetch content from S3 if not in cache + result = get_content_from_s3(key=kwargs.get("content_path")) + if not result: + logger.info( + "get_content_from_s3_view_no_valid_object", + key=kwargs.get("content_path"), + status_code=404, + ) + return HttpResponseNotFound("Page not found") + + content, content_type = result + # Store the result in cache + static_content_cache.set(cache_key, (content, content_type)) response = HttpResponse(content, content_type=content_type) logger.info(