diff --git a/channels/testing/live.py b/channels/testing/live.py index ce5ff1097..7522ccbf5 100644 --- a/channels/testing/live.py +++ b/channels/testing/live.py @@ -39,36 +39,44 @@ def live_server_url(self): def live_server_ws_url(self): return "ws://%s:%s" % (self.host, self._port) - def _pre_setup(self): + @classmethod + def setUpClass(cls): for connection in connections.all(): - if self._is_in_memory_db(connection): + if cls._is_in_memory_db(connection): raise ImproperlyConfigured( "ChannelLiveServerTestCase can not be used with in memory databases" ) - super(ChannelsLiveServerTestCase, self)._pre_setup() + super().setUpClass() - self._live_server_modified_settings = modify_settings( - ALLOWED_HOSTS={"append": self.host} + cls._live_server_modified_settings = modify_settings( + ALLOWED_HOSTS={"append": cls.host} ) - self._live_server_modified_settings.enable() + cls._live_server_modified_settings.enable() get_application = partial( make_application, - static_wrapper=self.static_wrapper if self.serve_static else None, + static_wrapper=cls.static_wrapper if cls.serve_static else None, ) - self._server_process = self.ProtocolServerProcess(self.host, get_application) - self._server_process.start() - self._server_process.ready.wait() - self._port = self._server_process.port.value + cls._server_process = cls.ProtocolServerProcess(cls.host, get_application) + cls._server_process.start() + while True: + if not cls._server_process.ready.wait(timeout=1): + if cls._server_process.is_alive(): + continue + raise RuntimeError("Server stopped") from None + break + cls._port = cls._server_process.port.value - def _post_teardown(self): - self._server_process.terminate() - self._server_process.join() - self._live_server_modified_settings.disable() - super(ChannelsLiveServerTestCase, self)._post_teardown() + @classmethod + def tearDownClass(cls): + cls._server_process.terminate() + cls._server_process.join() + cls._live_server_modified_settings.disable() + super().tearDownClass() - def _is_in_memory_db(self, connection): + @classmethod + def _is_in_memory_db(cls, connection): """ Check if DatabaseWrapper holds in memory database. """ diff --git a/setup.cfg b/setup.cfg index bc60b699c..551999c50 100644 --- a/setup.cfg +++ b/setup.cfg @@ -44,6 +44,7 @@ tests = pytest pytest-django pytest-asyncio + httpx daphne = daphne>=4.0.0 diff --git a/tests/conftest.py b/tests/conftest.py index 94c9803a7..54c3c6f58 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,26 +1,12 @@ +import os + import pytest from django.conf import settings def pytest_configure(): - settings.configure( - DATABASES={ - "default": { - "ENGINE": "django.db.backends.sqlite3", - # Override Django’s default behaviour of using an in-memory database - # in tests for SQLite, since that avoids connection.close() working. - "TEST": {"NAME": "test_db.sqlite3"}, - } - }, - INSTALLED_APPS=[ - "django.contrib.auth", - "django.contrib.contenttypes", - "django.contrib.sessions", - "django.contrib.admin", - "channels", - ], - SECRET_KEY="Not_a_secret_key", - ) + os.environ["DJANGO_SETTINGS_MODULE"] = "tests.testproject.settings" + settings._setup() def pytest_generate_tests(metafunc): diff --git a/tests/test_live_testcase.py b/tests/test_live_testcase.py new file mode 100644 index 000000000..098538339 --- /dev/null +++ b/tests/test_live_testcase.py @@ -0,0 +1,38 @@ +import httpx +from django.conf import settings + +from channels.testing import ChannelsLiveServerTestCase + + +def test_settings(): + assert settings.SETTINGS_MODULE == "tests.testproject.settings" + + +class TestLiveNoStatic(ChannelsLiveServerTestCase): + serve_static = False + + def test_properties(self): + # test properties + self.assertEqual( + self.live_server_ws_url, self.live_server_url.replace("http", "ws", 1) + ) + + def test_resolving(self): + result = httpx.get(f"{self.live_server_url}/admin", follow_redirects=True) + self.assertEqual(result.status_code, 200) + result = httpx.get(f"{self.live_server_url}/addsdsmidsdsn") + self.assertEqual(result.status_code, 404) + + +class TestLiveWithStatic(ChannelsLiveServerTestCase): + def test_properties(self): + # test properties + self.assertEqual( + self.live_server_ws_url, self.live_server_url.replace("http", "ws", 1) + ) + + def test_resolving(self): + result = httpx.get(f"{self.live_server_url}/admin", follow_redirects=True) + self.assertEqual(result.status_code, 200) + result = httpx.get(f"{self.live_server_url}/addsdsmidsdsn") + self.assertEqual(result.status_code, 404) diff --git a/tests/testproject/__init__.py b/tests/testproject/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/testproject/asgi.py b/tests/testproject/asgi.py new file mode 100644 index 000000000..d594c7df4 --- /dev/null +++ b/tests/testproject/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for testproject project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/5.2/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tests.testproject.settings") + +application = get_asgi_application() diff --git a/tests/testproject/settings.py b/tests/testproject/settings.py new file mode 100644 index 000000000..29f6b63e2 --- /dev/null +++ b/tests/testproject/settings.py @@ -0,0 +1,78 @@ +""" +Django settings for testproject project. + +Generated by 'django-admin startproject' using Django 5.2.3. + +For more information on this file, see +https://docs.djangoproject.com/en/5.2/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/5.2/ref/settings/ +""" + +SECRET_KEY = "Not_a_secret_key" + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = [] + + +# Application definition + +INSTALLED_APPS = [ + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "channels", +] + +MIDDLEWARE = [ + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", +] + +ROOT_URLCONF = "tests.testproject.urls" + +TEMPLATES = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + ], + }, + }, +] + + +# Database +# https://docs.djangoproject.com/en/5.2/ref/settings/#databases + +DATABASES = { + "default": { + "ENGINE": "django.db.backends.sqlite3", + # Override Django’s default behaviour of using an in-memory database + # in tests for SQLite, since that avoids connection.close() working. + "TEST": {"NAME": "test_db.sqlite3"}, + } +} + + +STATIC_URL = "static/" + +ASGI_APPLICATION = "tests.testproject.asgi.application" + +WSGI_APPLICATION = "tests.testproject.wsgi.application" diff --git a/tests/testproject/urls.py b/tests/testproject/urls.py new file mode 100644 index 000000000..319d1ace9 --- /dev/null +++ b/tests/testproject/urls.py @@ -0,0 +1,23 @@ +""" +URL configuration for testproject project. + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/5.2/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" + +from django.contrib import admin +from django.urls import path + +urlpatterns = [ + path("admin/", admin.site.urls), +] diff --git a/tests/testproject/wsgi.py b/tests/testproject/wsgi.py new file mode 100644 index 000000000..c9295e239 --- /dev/null +++ b/tests/testproject/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for testproject project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/5.2/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tests.testproject.settings") + +application = get_wsgi_application()