Skip to content

Commit 1e9777b

Browse files
system settings caching optimization + test cases (#13739)
1 parent b5569c0 commit 1e9777b

2 files changed

Lines changed: 192 additions & 2 deletions

File tree

dojo/middleware.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,8 @@ class DojoSytemSettingsMiddleware:
111111
def __init__(self, get_response):
112112
self.get_response = get_response
113113
from dojo.models import System_Settings # noqa: PLC0415 circular import
114-
models.signals.post_save.connect(self.cleanup, sender=System_Settings)
114+
# Use classmethod directly to avoid keeping reference to middleware instance
115+
models.signals.post_save.connect(DojoSytemSettingsMiddleware.cleanup, sender=System_Settings)
115116

116117
def __call__(self, request):
117118
self.load()

unittests/test_system_settings.py

Lines changed: 190 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
1-
from django.test import TestCase, override_settings
1+
from unittest.mock import Mock
2+
3+
from django.db import models
4+
from django.http import HttpResponse
5+
from django.test import RequestFactory, TestCase, override_settings
26
from django.urls import reverse
37
from django.utils.timezone import now
48

9+
from dojo.middleware import DojoSytemSettingsMiddleware
510
from dojo.models import (
611
Engagement,
712
Finding,
@@ -86,3 +91,187 @@ def test_post_request_initializes_form_with_finding_instance(self):
8691
data = {"close_reason": "Mitigated", "notes": "Closing this finding"}
8792
response = self.client.post(self.url, data)
8893
self.assertIn(response.status_code, [200, 302])
94+
95+
96+
class TestSystemSettingsMiddlewareIntegration(DojoTestCase):
97+
98+
"""Integration tests for DojoSytemSettingsMiddleware using RequestFactory."""
99+
100+
def setUp(self):
101+
"""Set up test environment."""
102+
super().setUp()
103+
self.factory = RequestFactory()
104+
# Ensure signal is connected
105+
models.signals.post_save.disconnect(DojoSytemSettingsMiddleware.cleanup, sender=System_Settings)
106+
models.signals.post_save.connect(DojoSytemSettingsMiddleware.cleanup, sender=System_Settings)
107+
108+
def test_middleware_loads_cache_on_request(self):
109+
"""Test that middleware loads settings into cache when processing a request."""
110+
# Ensure cache is empty
111+
DojoSytemSettingsMiddleware.cleanup()
112+
self.assertIsNone(DojoSytemSettingsMiddleware.get_system_settings())
113+
114+
# Create middleware with mock get_response
115+
mock_response = HttpResponse("OK")
116+
mock_get_response = Mock(return_value=mock_response)
117+
middleware = DojoSytemSettingsMiddleware(mock_get_response)
118+
119+
# Create a request
120+
request = self.factory.get("/test/")
121+
122+
# Process request through middleware
123+
response = middleware(request)
124+
125+
# Verify response is returned
126+
self.assertEqual(response, mock_response)
127+
mock_get_response.assert_called_once_with(request)
128+
129+
# Verify cache was populated during request processing
130+
# Note: cache should be cleaned up after request, but we can check during processing
131+
# Since cleanup happens in finally block, cache should be empty after __call__ returns
132+
self.assertIsNone(DojoSytemSettingsMiddleware.get_system_settings())
133+
134+
def test_middleware_cleans_up_cache_after_request(self):
135+
"""Test that middleware cleans up cache after request processing."""
136+
# Manually load cache first
137+
DojoSytemSettingsMiddleware.load()
138+
self.assertIsNotNone(DojoSytemSettingsMiddleware.get_system_settings())
139+
140+
# Create middleware
141+
middleware = DojoSytemSettingsMiddleware(lambda _r: HttpResponse("OK"))
142+
143+
# Process request
144+
request = self.factory.get("/test/")
145+
middleware(request)
146+
147+
# Verify cache is cleaned up after request
148+
self.assertIsNone(DojoSytemSettingsMiddleware.get_system_settings())
149+
150+
def test_middleware_cleans_up_cache_on_exception(self):
151+
"""Test that middleware cleans up cache even when exception occurs."""
152+
# Load cache first
153+
DojoSytemSettingsMiddleware.load()
154+
self.assertIsNotNone(DojoSytemSettingsMiddleware.get_system_settings())
155+
156+
# Create middleware that raises an exception
157+
def failing_get_response(request):
158+
msg = "Test exception"
159+
raise ValueError(msg)
160+
161+
middleware = DojoSytemSettingsMiddleware(failing_get_response)
162+
163+
# Process request - should raise exception
164+
request = self.factory.get("/test/")
165+
with self.assertRaises(ValueError):
166+
middleware(request)
167+
168+
# Verify cache is cleaned up even after exception
169+
self.assertIsNone(DojoSytemSettingsMiddleware.get_system_settings())
170+
171+
def test_middleware_process_exception_cleans_up_cache(self):
172+
"""Test that process_exception method cleans up cache."""
173+
# Load cache first
174+
DojoSytemSettingsMiddleware.load()
175+
self.assertIsNotNone(DojoSytemSettingsMiddleware.get_system_settings())
176+
177+
# Create middleware
178+
middleware = DojoSytemSettingsMiddleware(lambda _r: HttpResponse("OK"))
179+
180+
# Call process_exception directly
181+
request = self.factory.get("/test/")
182+
exception = ValueError("Test exception")
183+
middleware.process_exception(request, exception)
184+
185+
# Verify cache is cleaned up
186+
self.assertIsNone(DojoSytemSettingsMiddleware.get_system_settings())
187+
188+
def test_middleware_cache_isolation_between_requests(self):
189+
"""Test that cache is isolated between requests (thread-local)."""
190+
# Create middleware
191+
middleware = DojoSytemSettingsMiddleware(lambda _r: HttpResponse("OK"))
192+
193+
# First request
194+
request1 = self.factory.get("/test1/")
195+
middleware(request1)
196+
self.assertIsNone(DojoSytemSettingsMiddleware.get_system_settings())
197+
198+
# Second request - cache should be empty at start
199+
request2 = self.factory.get("/test2/")
200+
middleware(request2)
201+
self.assertIsNone(DojoSytemSettingsMiddleware.get_system_settings())
202+
203+
def test_middleware_cache_during_request_processing(self):
204+
"""Test that cache is available during request processing."""
205+
# Track if cache was available during request
206+
cache_available_during_request = []
207+
208+
def get_response_with_cache_check(request):
209+
# Check if cache is available during request processing
210+
cached = DojoSytemSettingsMiddleware.get_system_settings()
211+
cache_available_during_request.append(cached is not None)
212+
return HttpResponse("OK")
213+
214+
middleware = DojoSytemSettingsMiddleware(get_response_with_cache_check)
215+
216+
# Process request
217+
request = self.factory.get("/test/")
218+
middleware(request)
219+
220+
# Verify cache was available during request processing
221+
self.assertTrue(cache_available_during_request[0], "Cache should be available during request processing")
222+
223+
# But cleaned up after request
224+
self.assertIsNone(DojoSytemSettingsMiddleware.get_system_settings())
225+
226+
def test_multiple_get_calls_use_cache(self):
227+
"""Test that multiple calls to System_Settings.objects.get() use cache instead of multiple DB queries."""
228+
# Ensure cache is empty
229+
DojoSytemSettingsMiddleware.cleanup()
230+
231+
# First call should hit DB (cache is empty)
232+
with self.assertNumQueries(1):
233+
settings1 = System_Settings.objects.get()
234+
235+
# Load into cache via middleware
236+
DojoSytemSettingsMiddleware.load()
237+
238+
# Now multiple calls should use cache (no additional DB queries)
239+
with self.assertNumQueries(0):
240+
settings2 = System_Settings.objects.get()
241+
settings3 = System_Settings.objects.get()
242+
settings4 = System_Settings.objects.get()
243+
244+
# All calls should return the same cached object instance
245+
self.assertEqual(settings1.id, settings2.id)
246+
self.assertEqual(settings2.id, settings3.id)
247+
self.assertEqual(settings3.id, settings4.id)
248+
# Verify they're the same object instance (same memory address)
249+
self.assertIs(settings2, settings3)
250+
self.assertIs(settings3, settings4)
251+
252+
def test_multiple_get_calls_within_request_use_cache(self):
253+
"""Test that multiple get() calls within a single request use cache."""
254+
retrieved_settings = []
255+
256+
def get_response_with_multiple_gets(request):
257+
# Make multiple calls to get() during request processing
258+
retrieved_settings.append(System_Settings.objects.get())
259+
retrieved_settings.append(System_Settings.objects.get())
260+
retrieved_settings.append(System_Settings.objects.get())
261+
return HttpResponse("OK")
262+
263+
middleware = DojoSytemSettingsMiddleware(get_response_with_multiple_gets)
264+
265+
# Process request - should only hit DB once (when loading cache)
266+
# Then all subsequent get() calls should use cache
267+
request = self.factory.get("/test/")
268+
with self.assertNumQueries(1): # Only one query to load settings into cache
269+
middleware(request)
270+
271+
# Verify we got 3 settings objects
272+
self.assertEqual(len(retrieved_settings), 3)
273+
274+
# All should be the same cached instance
275+
self.assertIs(retrieved_settings[0], retrieved_settings[1])
276+
self.assertIs(retrieved_settings[1], retrieved_settings[2])
277+
self.assertEqual(retrieved_settings[0].id, retrieved_settings[1].id)

0 commit comments

Comments
 (0)