From 6cb639645415b7e1288e9d5becb92c53e6f06b56 Mon Sep 17 00:00:00 2001 From: Aman Pandey Date: Tue, 20 Aug 2024 23:58:07 +0530 Subject: [PATCH 01/13] ASGI check approach with added test and docs --- debug_toolbar/toolbar.py | 15 +++++++++------ docs/architecture.rst | 2 +- tests/test_integration.py | 18 +++++++++++++++++- 3 files changed, 27 insertions(+), 8 deletions(-) diff --git a/debug_toolbar/toolbar.py b/debug_toolbar/toolbar.py index 35d789a53..921546aaf 100644 --- a/debug_toolbar/toolbar.py +++ b/debug_toolbar/toolbar.py @@ -12,6 +12,7 @@ from django.apps import apps from django.conf import settings from django.core.exceptions import ImproperlyConfigured +from django.core.handlers.asgi import ASGIRequest from django.dispatch import Signal from django.template import TemplateSyntaxError from django.template.loader import render_to_string @@ -101,12 +102,14 @@ def should_render_panels(self): If False, the panels will be loaded via Ajax. """ if (render_panels := self.config["RENDER_PANELS"]) is None: - # If wsgi.multiprocess isn't in the headers, then it's likely - # being served by ASGI. This type of set up is most likely - # incompatible with the toolbar until - # https://github.com/jazzband/django-debug-toolbar/issues/1430 - # is resolved. - render_panels = self.request.META.get("wsgi.multiprocess", True) + # If wsgi.multiprocess is true then it is either being served + # from ASGI or multithreaded third-party WSGI server eg gunicorn. + # we need to make special handling for ASGI for supporting + # async context based requests. + if isinstance(self.request, ASGIRequest): + render_panels = False + else: + render_panels = self.request.META.get("wsgi.multiprocess", True) return render_panels # Handle storing toolbars in memory and fetching them later on diff --git a/docs/architecture.rst b/docs/architecture.rst index 0043f5153..cf5c54951 100644 --- a/docs/architecture.rst +++ b/docs/architecture.rst @@ -82,7 +82,7 @@ Problematic Parts - Support for async and multi-threading: ``debug_toolbar.middleware.DebugToolbarMiddleware`` is now async compatible and can process async requests. However certain panels such as ``SQLPanel``, ``TimerPanel``, - ``RequestPanel``, ``HistoryPanel`` and ``ProfilingPanel`` aren't fully + ``RequestPanel`` and ``ProfilingPanel`` aren't fully compatible and currently being worked on. For now, these panels are disabled by default when running in async environment. follow the progress of this issue in `Async compatible toolbar project `_. diff --git a/tests/test_integration.py b/tests/test_integration.py index df276d90c..ca31a294c 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -11,7 +11,7 @@ from django.db import connection from django.http import HttpResponse from django.template.loader import get_template -from django.test import RequestFactory +from django.test import AsyncRequestFactory, RequestFactory from django.test.utils import override_settings from debug_toolbar.forms import SignedDataForm @@ -126,6 +126,22 @@ def test_should_render_panels_multiprocess(self): request.META.pop("wsgi.multiprocess") self.assertTrue(toolbar.should_render_panels()) + def test_should_render_panels_asgi(self): + """ + The toolbar not should render the panels on each request when wsgi.multiprocess + is True or missing in case of async context rather than multithreaded + wsgi. + """ + async_request = AsyncRequestFactory().get("/") + # by default ASGIRequest will have wsgi.multiprocess set to True + # but we are still assigning this to true cause this could change + # and we specifically need to check that method returns false even with + # wsgi.multiprocess set to true + async_request.META["wsgi.multiprocess"] = True + toolbar = DebugToolbar(async_request, self.get_response) + toolbar.config["RENDER_PANELS"] = None + self.assertFalse(toolbar.should_render_panels()) + def _resolve_stats(self, path): # takes stats from Request panel request = rf.get(path) From 830c9641d1b4b9b196198d53eca8accee8d3e319 Mon Sep 17 00:00:00 2001 From: Aman Pandey Date: Wed, 21 Aug 2024 00:02:52 +0530 Subject: [PATCH 02/13] revert changes --- debug_toolbar/toolbar.py | 15 ++++++--------- docs/architecture.rst | 2 +- tests/test_integration.py | 18 +----------------- 3 files changed, 8 insertions(+), 27 deletions(-) diff --git a/debug_toolbar/toolbar.py b/debug_toolbar/toolbar.py index 921546aaf..35d789a53 100644 --- a/debug_toolbar/toolbar.py +++ b/debug_toolbar/toolbar.py @@ -12,7 +12,6 @@ from django.apps import apps from django.conf import settings from django.core.exceptions import ImproperlyConfigured -from django.core.handlers.asgi import ASGIRequest from django.dispatch import Signal from django.template import TemplateSyntaxError from django.template.loader import render_to_string @@ -102,14 +101,12 @@ def should_render_panels(self): If False, the panels will be loaded via Ajax. """ if (render_panels := self.config["RENDER_PANELS"]) is None: - # If wsgi.multiprocess is true then it is either being served - # from ASGI or multithreaded third-party WSGI server eg gunicorn. - # we need to make special handling for ASGI for supporting - # async context based requests. - if isinstance(self.request, ASGIRequest): - render_panels = False - else: - render_panels = self.request.META.get("wsgi.multiprocess", True) + # If wsgi.multiprocess isn't in the headers, then it's likely + # being served by ASGI. This type of set up is most likely + # incompatible with the toolbar until + # https://github.com/jazzband/django-debug-toolbar/issues/1430 + # is resolved. + render_panels = self.request.META.get("wsgi.multiprocess", True) return render_panels # Handle storing toolbars in memory and fetching them later on diff --git a/docs/architecture.rst b/docs/architecture.rst index cf5c54951..0043f5153 100644 --- a/docs/architecture.rst +++ b/docs/architecture.rst @@ -82,7 +82,7 @@ Problematic Parts - Support for async and multi-threading: ``debug_toolbar.middleware.DebugToolbarMiddleware`` is now async compatible and can process async requests. However certain panels such as ``SQLPanel``, ``TimerPanel``, - ``RequestPanel`` and ``ProfilingPanel`` aren't fully + ``RequestPanel``, ``HistoryPanel`` and ``ProfilingPanel`` aren't fully compatible and currently being worked on. For now, these panels are disabled by default when running in async environment. follow the progress of this issue in `Async compatible toolbar project `_. diff --git a/tests/test_integration.py b/tests/test_integration.py index ca31a294c..df276d90c 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -11,7 +11,7 @@ from django.db import connection from django.http import HttpResponse from django.template.loader import get_template -from django.test import AsyncRequestFactory, RequestFactory +from django.test import RequestFactory from django.test.utils import override_settings from debug_toolbar.forms import SignedDataForm @@ -126,22 +126,6 @@ def test_should_render_panels_multiprocess(self): request.META.pop("wsgi.multiprocess") self.assertTrue(toolbar.should_render_panels()) - def test_should_render_panels_asgi(self): - """ - The toolbar not should render the panels on each request when wsgi.multiprocess - is True or missing in case of async context rather than multithreaded - wsgi. - """ - async_request = AsyncRequestFactory().get("/") - # by default ASGIRequest will have wsgi.multiprocess set to True - # but we are still assigning this to true cause this could change - # and we specifically need to check that method returns false even with - # wsgi.multiprocess set to true - async_request.META["wsgi.multiprocess"] = True - toolbar = DebugToolbar(async_request, self.get_response) - toolbar.config["RENDER_PANELS"] = None - self.assertFalse(toolbar.should_render_panels()) - def _resolve_stats(self, path): # takes stats from Request panel request = rf.get(path) From d951d3444532b787e02fc845ee2515a1f0414d9e Mon Sep 17 00:00:00 2001 From: Aman Pandey Date: Tue, 3 Sep 2024 02:03:02 +0530 Subject: [PATCH 03/13] trial config of async env for tests --- debug_toolbar/middleware.py | 5 +++++ tests/asgi.py | 9 +++++++++ tests/settings.py | 3 +++ tests/test_integration.py | 6 ++++++ 4 files changed, 23 insertions(+) create mode 100644 tests/asgi.py diff --git a/debug_toolbar/middleware.py b/debug_toolbar/middleware.py index 03044f3a4..62659a7e5 100644 --- a/debug_toolbar/middleware.py +++ b/debug_toolbar/middleware.py @@ -102,6 +102,11 @@ def __call__(self, request): return self._postprocess(request, response, toolbar) async def __acall__(self, request): + # A flag to check if all the client calls are going through acall + # and envirnemnt is truely async + + print("__acall__ ran!") + # Decide whether the toolbar is active for this request. show_toolbar = get_show_toolbar() if not show_toolbar(request) or DebugToolbar.is_toolbar_request(request): diff --git a/tests/asgi.py b/tests/asgi.py new file mode 100644 index 000000000..acd2637a2 --- /dev/null +++ b/tests/asgi.py @@ -0,0 +1,9 @@ +"""ASGI config for testing.""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tests.settings") + +application = get_asgi_application() diff --git a/tests/settings.py b/tests/settings.py index 269900c18..a8bf9beb1 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -20,6 +20,7 @@ # Application definition INSTALLED_APPS = [ + "daphne", "django.contrib.admin", "django.contrib.auth", "django.contrib.contenttypes", @@ -30,6 +31,8 @@ "tests", ] +ASGI_APPLICATION = "tests.asgi.application" + USE_GIS = os.getenv("DB_BACKEND") == "postgis" if USE_GIS: diff --git a/tests/test_integration.py b/tests/test_integration.py index df276d90c..57990d802 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -285,10 +285,16 @@ def test_concurrent_async_sql_page(self): @override_settings(DEBUG=True) class DebugToolbarIntegrationTestCase(IntegrationTestCase): def test_middleware(self): + print("testing middleware now") response = self.client.get("/execute_sql/") self.assertEqual(response.status_code, 200) self.assertContains(response, "djDebug") + # async def test_middleware_in_async_mode(self): + # response = await self.async_client.get("/execute_sql/") + # self.assertEqual(response.status_code, 200) + # self.assertContains(response, "djDebug") + @override_settings(DEFAULT_CHARSET="iso-8859-1") def test_non_utf8_charset(self): response = self.client.get("/regular/ASCII/") From 4d555d4d710740947101841beda7d17c0de0616c Mon Sep 17 00:00:00 2001 From: Aman Pandey Date: Sat, 7 Sep 2024 02:38:10 +0530 Subject: [PATCH 04/13] add async test_integration.py --- tests/test_integration_async.py | 934 ++++++++++++++++++++++++++++++++ 1 file changed, 934 insertions(+) create mode 100644 tests/test_integration_async.py diff --git a/tests/test_integration_async.py b/tests/test_integration_async.py new file mode 100644 index 000000000..3effd82d0 --- /dev/null +++ b/tests/test_integration_async.py @@ -0,0 +1,934 @@ +import os +import re +import time +import unittest +from unittest.mock import patch + +import html5lib +from django.contrib.staticfiles.testing import StaticLiveServerTestCase +from django.core import signing +from django.core.cache import cache +from django.db import connection +from django.http import HttpResponse +from django.template.loader import get_template +from django.test import AsyncRequestFactory, RequestFactory +from django.test.utils import override_settings + +from debug_toolbar.forms import SignedDataForm +from debug_toolbar.middleware import DebugToolbarMiddleware, show_toolbar +from debug_toolbar.panels import Panel +from debug_toolbar.toolbar import DebugToolbar + +from .base import BaseTestCase, IntegrationTestCase +from .views import regular_view + +try: + from selenium import webdriver + from selenium.common.exceptions import NoSuchElementException + from selenium.webdriver.common.by import By + from selenium.webdriver.firefox.options import Options + from selenium.webdriver.support import expected_conditions as EC + from selenium.webdriver.support.wait import WebDriverWait +except ImportError: + webdriver = None + + +rf = RequestFactory() +af = AsyncRequestFactory() + + +def toolbar_store_id(): + def get_response(request): + return HttpResponse() + + toolbar = DebugToolbar(rf.get("/"), get_response) + toolbar.store() + return toolbar.store_id + + +class BuggyPanel(Panel): + def title(self): + return "BuggyPanel" + + @property + def content(self): + raise Exception + + +@override_settings(DEBUG=True) +class DebugToolbarTestCase(BaseTestCase): + def test_show_toolbar(self): + self.assertTrue(show_toolbar(self.request)) + + def test_show_toolbar_DEBUG(self): + with self.settings(DEBUG=False): + self.assertFalse(show_toolbar(self.request)) + + def test_show_toolbar_INTERNAL_IPS(self): + with self.settings(INTERNAL_IPS=[]): + self.assertFalse(show_toolbar(self.request)) + + @patch("socket.gethostbyname", return_value="127.0.0.255") + def test_show_toolbar_docker(self, mocked_gethostbyname): + with self.settings(INTERNAL_IPS=[]): + # Is true because REMOTE_ADDR is 127.0.0.1 and the 255 + # is shifted to be 1. + self.assertTrue(show_toolbar(self.request)) + mocked_gethostbyname.assert_called_once_with("host.docker.internal") + + def test_not_iterating_over_INTERNAL_IPS(self): + """Verify that the middleware does not iterate over INTERNAL_IPS in some way. + + Some people use iptools.IpRangeList for their INTERNAL_IPS. This is a class + that can quickly answer the question if the setting contain a certain IP address, + but iterating over this object will drain all performance / blow up. + """ + + class FailOnIteration: + def __iter__(self): + raise RuntimeError( + "The testcase failed: the code should not have iterated over INTERNAL_IPS" + ) + + def __contains__(self, x): + return True + + with self.settings(INTERNAL_IPS=FailOnIteration()): + response = self.client.get("/regular/basic/") + self.assertEqual(response.status_code, 200) + self.assertContains(response, "djDebug") # toolbar + + def test_should_render_panels_RENDER_PANELS(self): + """ + The toolbar should force rendering panels on each request + based on the RENDER_PANELS setting. + """ + toolbar = DebugToolbar(self.request, self.get_response) + self.assertFalse(toolbar.should_render_panels()) + toolbar.config["RENDER_PANELS"] = True + self.assertTrue(toolbar.should_render_panels()) + toolbar.config["RENDER_PANELS"] = None + self.assertTrue(toolbar.should_render_panels()) + + def test_should_render_panels_multiprocess(self): + """ + The toolbar should render the panels on each request when wsgi.multiprocess + is True or missing. + """ + request = rf.get("/") + request.META["wsgi.multiprocess"] = True + toolbar = DebugToolbar(request, self.get_response) + toolbar.config["RENDER_PANELS"] = None + self.assertTrue(toolbar.should_render_panels()) + + request.META["wsgi.multiprocess"] = False + self.assertFalse(toolbar.should_render_panels()) + + request.META.pop("wsgi.multiprocess") + self.assertTrue(toolbar.should_render_panels()) + + def _resolve_stats(self, path): + # takes stats from Request panel + request = rf.get(path) + panel = self.toolbar.get_panel_by_id("RequestPanel") + response = panel.process_request(request) + panel.generate_stats(request, response) + return panel.get_stats() + + def test_url_resolving_positional(self): + stats = self._resolve_stats("/resolving1/a/b/") + self.assertEqual(stats["view_urlname"], "positional-resolving") + self.assertEqual(stats["view_func"], "tests.views.resolving_view") + self.assertEqual(stats["view_args"], ("a", "b")) + self.assertEqual(stats["view_kwargs"], {}) + + def test_url_resolving_named(self): + stats = self._resolve_stats("/resolving2/a/b/") + self.assertEqual(stats["view_args"], ()) + self.assertEqual(stats["view_kwargs"], {"arg1": "a", "arg2": "b"}) + + def test_url_resolving_mixed(self): + stats = self._resolve_stats("/resolving3/a/") + self.assertEqual(stats["view_args"], ("a",)) + self.assertEqual(stats["view_kwargs"], {"arg2": "default"}) + + def test_url_resolving_bad(self): + stats = self._resolve_stats("/non-existing-url/") + self.assertEqual(stats["view_urlname"], "None") + self.assertEqual(stats["view_args"], "None") + self.assertEqual(stats["view_kwargs"], "None") + self.assertEqual(stats["view_func"], "") + + def test_middleware_response_insertion(self): + def get_response(request): + return regular_view(request, "İ") + + response = DebugToolbarMiddleware(get_response)(self.request) + # check toolbar insertion before "" + self.assertContains(response, "\n") + + def test_middleware_no_injection_when_encoded(self): + def get_response(request): + response = HttpResponse("") + response["Content-Encoding"] = "something" + return response + + response = DebugToolbarMiddleware(get_response)(self.request) + self.assertEqual(response.content, b"") + + def test_cache_page(self): + # Clear the cache before testing the views. Other tests that use cached_view + # may run earlier and cause fewer cache calls. + cache.clear() + response = self.client.get("/cached_view/") + self.assertEqual(len(response.toolbar.get_panel_by_id("CachePanel").calls), 3) + response = self.client.get("/cached_view/") + self.assertEqual(len(response.toolbar.get_panel_by_id("CachePanel").calls), 2) + + @override_settings(ROOT_URLCONF="tests.urls_use_package_urls") + def test_include_package_urls(self): + """Test urlsconf that uses the debug_toolbar.urls in the include call""" + # Clear the cache before testing the views. Other tests that use cached_view + # may run earlier and cause fewer cache calls. + cache.clear() + response = self.client.get("/cached_view/") + self.assertEqual(len(response.toolbar.get_panel_by_id("CachePanel").calls), 3) + response = self.client.get("/cached_view/") + self.assertEqual(len(response.toolbar.get_panel_by_id("CachePanel").calls), 2) + + def test_low_level_cache_view(self): + """Test cases when low level caching API is used within a request.""" + response = self.client.get("/cached_low_level_view/") + self.assertEqual(len(response.toolbar.get_panel_by_id("CachePanel").calls), 2) + response = self.client.get("/cached_low_level_view/") + self.assertEqual(len(response.toolbar.get_panel_by_id("CachePanel").calls), 1) + + def test_cache_disable_instrumentation(self): + """ + Verify that middleware cache usages before and after + DebugToolbarMiddleware are not counted. + """ + self.assertIsNone(cache.set("UseCacheAfterToolbar.before", None)) + self.assertIsNone(cache.set("UseCacheAfterToolbar.after", None)) + response = self.client.get("/execute_sql/") + self.assertEqual(cache.get("UseCacheAfterToolbar.before"), 1) + self.assertEqual(cache.get("UseCacheAfterToolbar.after"), 1) + self.assertEqual(len(response.toolbar.get_panel_by_id("CachePanel").calls), 0) + + def test_is_toolbar_request(self): + request = rf.get("/__debug__/render_panel/") + self.assertTrue(self.toolbar.is_toolbar_request(request)) + + request = rf.get("/invalid/__debug__/render_panel/") + self.assertFalse(self.toolbar.is_toolbar_request(request)) + + request = rf.get("/render_panel/") + self.assertFalse(self.toolbar.is_toolbar_request(request)) + + @override_settings(ROOT_URLCONF="tests.urls_invalid") + def test_is_toolbar_request_without_djdt_urls(self): + """Test cases when the toolbar urls aren't configured.""" + request = rf.get("/__debug__/render_panel/") + self.assertFalse(self.toolbar.is_toolbar_request(request)) + + request = rf.get("/render_panel/") + self.assertFalse(self.toolbar.is_toolbar_request(request)) + + @override_settings(ROOT_URLCONF="tests.urls_invalid") + def test_is_toolbar_request_override_request_urlconf(self): + """Test cases when the toolbar URL is configured on the request.""" + request = rf.get("/__debug__/render_panel/") + self.assertFalse(self.toolbar.is_toolbar_request(request)) + + # Verify overriding the urlconf on the request is valid. + request.urlconf = "tests.urls" + self.assertTrue(self.toolbar.is_toolbar_request(request)) + + def test_is_toolbar_request_with_script_prefix(self): + """ + Test cases when Django is running under a path prefix, such as via the + FORCE_SCRIPT_NAME setting. + """ + request = rf.get("/__debug__/render_panel/", SCRIPT_NAME="/path/") + self.assertTrue(self.toolbar.is_toolbar_request(request)) + + request = rf.get("/invalid/__debug__/render_panel/", SCRIPT_NAME="/path/") + self.assertFalse(self.toolbar.is_toolbar_request(request)) + + request = rf.get("/render_panel/", SCRIPT_NAME="/path/") + self.assertFalse(self.toolbar.is_toolbar_request(self.request)) + + def test_data_gone(self): + response = self.client.get( + "/__debug__/render_panel/?store_id=GONE&panel_id=RequestPanel" + ) + self.assertIn("Please reload the page and retry.", response.json()["content"]) + + def test_sql_page(self): + response = self.client.get("/execute_sql/") + self.assertEqual( + len(response.toolbar.get_panel_by_id("SQLPanel").get_stats()["queries"]), 1 + ) + + def test_async_sql_page(self): + response = self.client.get("/async_execute_sql/") + self.assertEqual( + len(response.toolbar.get_panel_by_id("SQLPanel").get_stats()["queries"]), 1 + ) + + def test_concurrent_async_sql_page(self): + response = self.client.get("/async_execute_sql_concurrently/") + self.assertEqual( + len(response.toolbar.get_panel_by_id("SQLPanel").get_stats()["queries"]), 2 + ) + + +@override_settings(DEBUG=True) +class DebugToolbarIntegrationTestCase(IntegrationTestCase): + # def test_middleware(self): + # response = self.client.get("/execute_sql/") + # self.assertEqual(response.status_code, 200) + # self.assertContains(response, "djDebug") + + async def test_middleware_in_async_mode(self): + response = await self.async_client.get("/async_execute_sql/") + self.assertEqual(response.status_code, 200) + self.assertContains(response, "djDebug") + + @override_settings(DEFAULT_CHARSET="iso-8859-1") + async def test_non_utf8_charset(self): + response = await self.async_client.get("/regular/ASCII/") + self.assertContains(response, "ASCII") # template + self.assertContains(response, "djDebug") # toolbar + + response = self.client.get("/regular/LÀTÍN/") + self.assertContains(response, "LÀTÍN") # template + self.assertContains(response, "djDebug") # toolbar + + async def test_html5_validation(self): + response = await self.async_client.get("/regular/HTML5/") + parser = html5lib.HTMLParser() + content = response.content + parser.parse(content) + if parser.errors: + default_msg = ["Content is invalid HTML:"] + lines = content.split(b"\n") + for position, errorcode, datavars in parser.errors: + default_msg.append(" %s" % html5lib.constants.E[errorcode] % datavars) + default_msg.append(" %r" % lines[position[0] - 1]) + msg = self._formatMessage(None, "\n".join(default_msg)) + raise self.failureException(msg) + + async def test_render_panel_checks_show_toolbar(self): + url = "/__debug__/render_panel/" + data = {"store_id": toolbar_store_id(), "panel_id": "VersionsPanel"} + + response = await self.async_client.get(url, data) + self.assertEqual(response.status_code, 200) + response = await self.async_client.get( + url, data, headers={"x-requested-with": "XMLHttpRequest"} + ) + self.assertEqual(response.status_code, 200) + with self.settings(INTERNAL_IPS=[]): + response = await self.async_client.get(url, data) + self.assertEqual(response.status_code, 404) + response = await self.async_client.get( + url, data, headers={"x-requested-with": "XMLHttpRequest"} + ) + self.assertEqual(response.status_code, 404) + + async def test_middleware_render_toolbar_json(self): + """Verify the toolbar is rendered and data is stored for a json request.""" + self.assertEqual(len(DebugToolbar._store), 0) + + data = {"foo": "bar"} + response = await self.async_client.get( + "/json_view/", data, content_type="application/json" + ) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.content.decode("utf-8"), '{"foo": "bar"}') + # Check the history panel's stats to verify the toolbar rendered properly. + self.assertEqual(len(DebugToolbar._store), 1) + toolbar = list(DebugToolbar._store.values())[0] + print(toolbar.get_panel_by_id("HistoryPanel").get_stats()) + self.assertEqual( + toolbar.get_panel_by_id("HistoryPanel").get_stats()["data"], + {"foo": ["bar"]}, + ) + + def test_template_source_checks_show_toolbar(self): + template = get_template("basic.html") + url = "/__debug__/template_source/" + data = { + "template": template.template.name, + "template_origin": signing.dumps(template.template.origin.name), + } + + response = self.client.get(url, data) + self.assertEqual(response.status_code, 200) + response = self.client.get( + url, data, headers={"x-requested-with": "XMLHttpRequest"} + ) + self.assertEqual(response.status_code, 200) + with self.settings(INTERNAL_IPS=[]): + response = self.client.get(url, data) + self.assertEqual(response.status_code, 404) + response = self.client.get( + url, data, headers={"x-requested-with": "XMLHttpRequest"} + ) + self.assertEqual(response.status_code, 404) + + def test_template_source_errors(self): + url = "/__debug__/template_source/" + + response = self.client.get(url, {}) + self.assertContains( + response, '"template_origin" key is required', status_code=400 + ) + + template = get_template("basic.html") + response = self.client.get( + url, + {"template_origin": signing.dumps(template.template.origin.name) + "xyz"}, + ) + self.assertContains(response, '"template_origin" is invalid', status_code=400) + + response = self.client.get( + url, {"template_origin": signing.dumps("does_not_exist.html")} + ) + self.assertContains(response, "Template Does Not Exist: does_not_exist.html") + + def test_sql_select_checks_show_toolbar(self): + url = "/__debug__/sql_select/" + data = { + "signed": SignedDataForm.sign( + { + "sql": "SELECT * FROM auth_user", + "raw_sql": "SELECT * FROM auth_user", + "params": "{}", + "alias": "default", + "duration": "0", + } + ) + } + + response = self.client.post(url, data) + self.assertEqual(response.status_code, 200) + response = self.client.post( + url, data, headers={"x-requested-with": "XMLHttpRequest"} + ) + self.assertEqual(response.status_code, 200) + with self.settings(INTERNAL_IPS=[]): + response = self.client.post(url, data) + self.assertEqual(response.status_code, 404) + response = self.client.post( + url, data, headers={"x-requested-with": "XMLHttpRequest"} + ) + self.assertEqual(response.status_code, 404) + + def test_sql_explain_checks_show_toolbar(self): + url = "/__debug__/sql_explain/" + data = { + "signed": SignedDataForm.sign( + { + "sql": "SELECT * FROM auth_user", + "raw_sql": "SELECT * FROM auth_user", + "params": "{}", + "alias": "default", + "duration": "0", + } + ) + } + + response = self.client.post(url, data) + self.assertEqual(response.status_code, 200) + response = self.client.post( + url, data, headers={"x-requested-with": "XMLHttpRequest"} + ) + self.assertEqual(response.status_code, 200) + with self.settings(INTERNAL_IPS=[]): + response = self.client.post(url, data) + self.assertEqual(response.status_code, 404) + response = self.client.post( + url, data, headers={"x-requested-with": "XMLHttpRequest"} + ) + self.assertEqual(response.status_code, 404) + + @unittest.skipUnless( + connection.vendor == "postgresql", "Test valid only on PostgreSQL" + ) + def test_sql_explain_postgres_union_query(self): + """ + Confirm select queries that start with a parenthesis can be explained. + """ + url = "/__debug__/sql_explain/" + data = { + "signed": SignedDataForm.sign( + { + "sql": "(SELECT * FROM auth_user) UNION (SELECT * from auth_user)", + "raw_sql": "(SELECT * FROM auth_user) UNION (SELECT * from auth_user)", + "params": "{}", + "alias": "default", + "duration": "0", + } + ) + } + + response = self.client.post(url, data) + self.assertEqual(response.status_code, 200) + + @unittest.skipUnless( + connection.vendor == "postgresql", "Test valid only on PostgreSQL" + ) + def test_sql_explain_postgres_json_field(self): + url = "/__debug__/sql_explain/" + base_query = ( + 'SELECT * FROM "tests_postgresjson" WHERE "tests_postgresjson"."field" @>' + ) + query = base_query + """ '{"foo": "bar"}'""" + data = { + "signed": SignedDataForm.sign( + { + "sql": query, + "raw_sql": base_query + " %s", + "params": '["{\\"foo\\": \\"bar\\"}"]', + "alias": "default", + "duration": "0", + } + ) + } + response = self.client.post(url, data) + self.assertEqual(response.status_code, 200) + response = self.client.post( + url, data, headers={"x-requested-with": "XMLHttpRequest"} + ) + self.assertEqual(response.status_code, 200) + with self.settings(INTERNAL_IPS=[]): + response = self.client.post(url, data) + self.assertEqual(response.status_code, 404) + response = self.client.post( + url, data, headers={"x-requested-with": "XMLHttpRequest"} + ) + self.assertEqual(response.status_code, 404) + + def test_sql_profile_checks_show_toolbar(self): + url = "/__debug__/sql_profile/" + data = { + "signed": SignedDataForm.sign( + { + "sql": "SELECT * FROM auth_user", + "raw_sql": "SELECT * FROM auth_user", + "params": "{}", + "alias": "default", + "duration": "0", + } + ) + } + + response = self.client.post(url, data) + self.assertEqual(response.status_code, 200) + response = self.client.post( + url, data, headers={"x-requested-with": "XMLHttpRequest"} + ) + self.assertEqual(response.status_code, 200) + with self.settings(INTERNAL_IPS=[]): + response = self.client.post(url, data) + self.assertEqual(response.status_code, 404) + response = self.client.post( + url, data, headers={"x-requested-with": "XMLHttpRequest"} + ) + self.assertEqual(response.status_code, 404) + + @override_settings(DEBUG_TOOLBAR_CONFIG={"RENDER_PANELS": True}) + def test_render_panels_in_request(self): + """ + Test that panels are are rendered during the request with + RENDER_PANELS=TRUE + """ + url = "/regular/basic/" + response = self.client.get(url) + self.assertIn(b'id="djDebug"', response.content) + # Verify the store id is not included. + self.assertNotIn(b"data-store-id", response.content) + # Verify the history panel was disabled + self.assertIn( + b'', + response.content, + ) + # Verify the a panel was rendered + self.assertIn(b"Response headers", response.content) + + @override_settings(DEBUG_TOOLBAR_CONFIG={"RENDER_PANELS": False}) + def test_load_panels(self): + """ + Test that panels are not rendered during the request with + RENDER_PANELS=False + """ + url = "/execute_sql/" + response = self.client.get(url) + self.assertIn(b'id="djDebug"', response.content) + # Verify the store id is included. + self.assertIn(b"data-store-id", response.content) + # Verify the history panel was not disabled + self.assertNotIn( + b'', + response.content, + ) + # Verify the a panel was not rendered + self.assertNotIn(b"Response headers", response.content) + + def test_view_returns_template_response(self): + response = self.client.get("/template_response/basic/") + self.assertEqual(response.status_code, 200) + + @override_settings(DEBUG_TOOLBAR_CONFIG={"DISABLE_PANELS": set()}) + def test_intercept_redirects(self): + response = self.client.get("/redirect/") + self.assertEqual(response.status_code, 200) + # Link to LOCATION header. + self.assertIn(b'href="/regular/redirect/"', response.content) + + def test_server_timing_headers(self): + response = self.client.get("/execute_sql/") + server_timing = response["Server-Timing"] + expected_partials = [ + r'TimerPanel_utime;dur=(\d)*(\.(\d)*)?;desc="User CPU time", ', + r'TimerPanel_stime;dur=(\d)*(\.(\d)*)?;desc="System CPU time", ', + r'TimerPanel_total;dur=(\d)*(\.(\d)*)?;desc="Total CPU time", ', + r'TimerPanel_total_time;dur=(\d)*(\.(\d)*)?;desc="Elapsed time", ', + r'SQLPanel_sql_time;dur=(\d)*(\.(\d)*)?;desc="SQL 1 queries", ', + r'CachePanel_total_time;dur=0;desc="Cache 0 Calls"', + ] + for expected in expected_partials: + self.assertTrue(re.compile(expected).search(server_timing)) + + @override_settings(DEBUG_TOOLBAR_CONFIG={"RENDER_PANELS": True}) + def test_timer_panel(self): + response = self.client.get("/regular/basic/") + self.assertEqual(response.status_code, 200) + self.assertContains( + response, + '