diff --git a/packages/runtime-sdk/tests/conftest.py b/packages/runtime-sdk/tests/conftest.py index 07e4e424..8308342e 100644 --- a/packages/runtime-sdk/tests/conftest.py +++ b/packages/runtime-sdk/tests/conftest.py @@ -192,10 +192,15 @@ def dev_server( @functools.cache -def get_suite_results(dev_server: str, suite: str) -> SuiteResults | str: +def get_suite_results( + dev_server: str, suite: str, mode: str | None = None +) -> SuiteResults | str: + url = f"{dev_server}/run-tests/{suite}" + if mode is not None: + url = f"{url}?mode={mode}" try: resp = requests.get( - f"{dev_server}/run-tests/{suite}", + url, timeout=(SUITE_CONNECT_TIMEOUT, SUITE_READ_TIMEOUT), ) except requests.RequestException as error: @@ -205,9 +210,9 @@ def get_suite_results(dev_server: str, suite: str) -> SuiteResults | str: return resp.json() -def _make_test(suite: str, test_name: str) -> Callable: +def _make_test(suite: str, test_name: str, mode: str | None = None) -> Callable: def test_fn(self: Any, dev_server: str) -> None: - results = get_suite_results(dev_server, suite) + results = get_suite_results(dev_server, suite, mode) if isinstance(results, str): pytest.fail(results) return @@ -224,12 +229,17 @@ def test_fn(self: Any, dev_server: str) -> None: return test_fn -def make_suite_class(suite: str, tests: list[str]) -> type: +def make_suite_class(suite: str, tests: list[str], mode: str | None = None) -> type: """Build a test class with one method per in-worker test of `suite`.""" + name = ( + f"Test{suite.upper()}" + if mode is None + else f"Test{suite.upper()}_{mode.upper()}" + ) return type( - f"Test{suite.upper()}", + name, (), - {f"test_{name}": _make_test(suite, name) for name in tests}, + {f"test_{test}": _make_test(suite, test, mode) for test in tests}, ) @@ -256,12 +266,16 @@ def discover_suites(src_dir: Path) -> dict[str, list[str]]: } -def register_in_worker_suites(namespace: dict[str, Any], src_dir: Path) -> None: +def register_in_worker_suites( + namespace: dict[str, Any], src_dir: Path, mode: str | None = None +) -> None: """Define a ``TestXxx`` class in `namespace` for every suite found in `src_dir`. Call with ``globals()`` from a test module so each in-worker test surfaces as - its own pytest case without manual registration. + its own pytest case without manual registration. Pass `mode` to run the same + suites again against a different in-worker app; tests that do not apply to a + mode report themselves as skipped. """ for suite, test_names in discover_suites(src_dir).items(): - suite_cls = make_suite_class(suite, test_names) + suite_cls = make_suite_class(suite, test_names, mode) namespace[suite_cls.__name__] = suite_cls diff --git a/packages/runtime-sdk/tests/test_web_frameworks.py b/packages/runtime-sdk/tests/test_django.py similarity index 70% rename from packages/runtime-sdk/tests/test_web_frameworks.py rename to packages/runtime-sdk/tests/test_django.py index fb35071c..2cf90713 100644 --- a/packages/runtime-sdk/tests/test_web_frameworks.py +++ b/packages/runtime-sdk/tests/test_django.py @@ -3,6 +3,10 @@ To add a new framework: create a subdirectory under web-frameworks-test/ with worker.py, wrangler.jsonc, pyproject.toml, and test_*.py files. +Each Django suite runs twice against the same worker: once through the ASGI +handler (async views) and once through the WSGI handler (the sync mirror in +``django_app.urls_sync``), so one set of test files covers both adaptors. + Python 3.12 (Pyodide 0.26.0a2) is excluded. The in-worker pytest suite drives async tests via ``loop.run_until_complete``, which is a no-op on Pyodide 0.26.0a2 (no ``run_sync``/JSPI): async tests return unawaited futures and report false @@ -14,9 +18,7 @@ import pytest from conftest import COMPAT_CONFIGS, CompatConfig, register_in_worker_suites -WEB_FRAMEWORKS_DIR: Path = ( - Path(__file__).parent / "web-frameworks-test" / "django-async" -) +WEB_FRAMEWORKS_DIR: Path = Path(__file__).parent / "web-frameworks-test" / "django" WEB_FRAMEWORKS_SRC_DIR: Path = WEB_FRAMEWORKS_DIR / "src" @@ -38,4 +40,5 @@ def compat_config(request: pytest.FixtureRequest) -> CompatConfig: return request.param -register_in_worker_suites(globals(), WEB_FRAMEWORKS_SRC_DIR) +register_in_worker_suites(globals(), WEB_FRAMEWORKS_SRC_DIR, mode="asgi") +register_in_worker_suites(globals(), WEB_FRAMEWORKS_SRC_DIR, mode="wsgi") diff --git a/packages/runtime-sdk/tests/web-frameworks-test/django-async/src/test_caching.py b/packages/runtime-sdk/tests/web-frameworks-test/django-async/src/test_caching.py deleted file mode 100644 index a32cc543..00000000 --- a/packages/runtime-sdk/tests/web-frameworks-test/django-async/src/test_caching.py +++ /dev/null @@ -1,62 +0,0 @@ -import pytest -from _client import get_json, post_json, read_json - - -@pytest.mark.asyncio -async def test_cache_set_and_get(django_asgi_app): - response = await post_json( - django_asgi_app, "/cache/set/", {"key": "k1", "value": "v1"} - ) - assert response.status == 200 - assert await read_json(response) == {"cached": True, "key": "k1", "value": "v1"} - - response, payload = await get_json(django_asgi_app, "/cache/get/?key=k1") - assert response.status == 200 - assert payload == {"key": "k1", "value": "v1"} - - -@pytest.mark.asyncio -async def test_cache_delete_and_missing(django_asgi_app): - await post_json( - django_asgi_app, "/cache/set/", {"key": "delete-key", "value": "delete-value"} - ) - response, payload = await get_json(django_asgi_app, "/cache/delete/?key=delete-key") - assert response.status == 200 - assert payload == {"deleted": True, "key": "delete-key"} - - response, payload = await get_json(django_asgi_app, "/cache/get/?key=delete-key") - assert response.status == 200 - assert payload == {"key": "delete-key", "value": None} - - response, payload = await get_json(django_asgi_app, "/cache/get/?key=nonexistent") - assert response.status == 200 - assert payload == {"key": "nonexistent", "value": None} - - -@pytest.mark.asyncio -async def test_cache_clear(django_asgi_app): - for key, value in (("clear-key-1", "value-1"), ("clear-key-2", "value-2")): - await post_json(django_asgi_app, "/cache/set/", {"key": key, "value": value}) - - response, payload = await get_json(django_asgi_app, "/cache/clear/") - assert response.status == 200 - assert payload["cleared"] is True - - for key in ("clear-key-1", "clear-key-2"): - response, payload = await get_json(django_asgi_app, f"/cache/get/?key={key}") - assert response.status == 200 - assert payload == {"key": key, "value": None} - - -@pytest.mark.asyncio -async def test_cache_overwrite(django_asgi_app): - await post_json( - django_asgi_app, "/cache/set/", {"key": "overwrite-key", "value": "value-1"} - ) - await post_json( - django_asgi_app, "/cache/set/", {"key": "overwrite-key", "value": "value-2"} - ) - - response, payload = await get_json(django_asgi_app, "/cache/get/?key=overwrite-key") - assert response.status == 200 - assert payload == {"key": "overwrite-key", "value": "value-2"} diff --git a/packages/runtime-sdk/tests/web-frameworks-test/django-async/src/test_streaming.py b/packages/runtime-sdk/tests/web-frameworks-test/django-async/src/test_streaming.py deleted file mode 100644 index 3467068f..00000000 --- a/packages/runtime-sdk/tests/web-frameworks-test/django-async/src/test_streaming.py +++ /dev/null @@ -1,22 +0,0 @@ -import pytest -from _client import fetch - - -@pytest.mark.asyncio -async def test_streaming_generators(django_asgi_app): - for path in ("/stream/async-gen/", "/stream/sync-gen/"): - response = await fetch(django_asgi_app, path) - body = await response.text() - - assert response.status == 200 - for index in range(5): - assert f"chunk-{index}" in body - - -@pytest.mark.asyncio -async def test_streaming_content_type(django_asgi_app): - response = await fetch(django_asgi_app, "/stream/async-gen/") - - assert response.status == 200 - assert response.headers.get("Content-Type") is not None - assert response.headers.get("Content-Type").startswith("text/plain") diff --git a/packages/runtime-sdk/tests/web-frameworks-test/django-async/pyproject.toml b/packages/runtime-sdk/tests/web-frameworks-test/django/pyproject.toml similarity index 87% rename from packages/runtime-sdk/tests/web-frameworks-test/django-async/pyproject.toml rename to packages/runtime-sdk/tests/web-frameworks-test/django/pyproject.toml index 00320f75..e1848ce4 100644 --- a/packages/runtime-sdk/tests/web-frameworks-test/django-async/pyproject.toml +++ b/packages/runtime-sdk/tests/web-frameworks-test/django/pyproject.toml @@ -1,5 +1,5 @@ [project] -name = "django-async-test" +name = "django-test" version = "0.1.0" requires-python = ">=3.12" dependencies = [ diff --git a/packages/runtime-sdk/tests/web-frameworks-test/django-async/src/_client.py b/packages/runtime-sdk/tests/web-frameworks-test/django/src/_client.py similarity index 92% rename from packages/runtime-sdk/tests/web-frameworks-test/django-async/src/_client.py rename to packages/runtime-sdk/tests/web-frameworks-test/django/src/_client.py index ad205231..f84cb0e4 100644 --- a/packages/runtime-sdk/tests/web-frameworks-test/django-async/src/_client.py +++ b/packages/runtime-sdk/tests/web-frameworks-test/django/src/_client.py @@ -1,7 +1,9 @@ import json as _json +from django.core.handlers.wsgi import WSGIHandler + import asgi -from workers import Request +from workers import Request, wsgi BASE_URL = "http://testserver" @@ -18,6 +20,10 @@ def _with_content_length(headers, body): return hdrs +def is_wsgi(app): + return isinstance(app, WSGIHandler) + + async def fetch(app, path, method="GET", headers=None, body=None): request = Request( f"{BASE_URL}{path}", @@ -25,6 +31,8 @@ async def fetch(app, path, method="GET", headers=None, body=None): headers=_with_content_length(headers, body), body=body, ) + if is_wsgi(app): + return await wsgi.fetch(app, request, {}) return await asgi.fetch(app, request, {}) diff --git a/packages/runtime-sdk/tests/web-frameworks-test/django-async/src/conftest.py b/packages/runtime-sdk/tests/web-frameworks-test/django/src/conftest.py similarity index 100% rename from packages/runtime-sdk/tests/web-frameworks-test/django-async/src/conftest.py rename to packages/runtime-sdk/tests/web-frameworks-test/django/src/conftest.py diff --git a/packages/runtime-sdk/tests/web-frameworks-test/django-async/src/django_app/__init__.py b/packages/runtime-sdk/tests/web-frameworks-test/django/src/django_app/__init__.py similarity index 100% rename from packages/runtime-sdk/tests/web-frameworks-test/django-async/src/django_app/__init__.py rename to packages/runtime-sdk/tests/web-frameworks-test/django/src/django_app/__init__.py diff --git a/packages/runtime-sdk/tests/web-frameworks-test/django-async/src/django_app/api_urls.py b/packages/runtime-sdk/tests/web-frameworks-test/django/src/django_app/api_urls.py similarity index 88% rename from packages/runtime-sdk/tests/web-frameworks-test/django-async/src/django_app/api_urls.py rename to packages/runtime-sdk/tests/web-frameworks-test/django/src/django_app/api_urls.py index 498c5e19..6cab9dcd 100644 --- a/packages/runtime-sdk/tests/web-frameworks-test/django-async/src/django_app/api_urls.py +++ b/packages/runtime-sdk/tests/web-frameworks-test/django/src/django_app/api_urls.py @@ -4,7 +4,7 @@ app_name = "api" -async def info(request): +def info(request): return JsonResponse({"namespace": "api-v1"}) diff --git a/packages/runtime-sdk/tests/web-frameworks-test/django-async/src/django_app/auth_backend.py b/packages/runtime-sdk/tests/web-frameworks-test/django/src/django_app/auth_backend.py similarity index 100% rename from packages/runtime-sdk/tests/web-frameworks-test/django-async/src/django_app/auth_backend.py rename to packages/runtime-sdk/tests/web-frameworks-test/django/src/django_app/auth_backend.py diff --git a/packages/runtime-sdk/tests/web-frameworks-test/django-async/src/django_app/drf_auth.py b/packages/runtime-sdk/tests/web-frameworks-test/django/src/django_app/drf_auth.py similarity index 100% rename from packages/runtime-sdk/tests/web-frameworks-test/django-async/src/django_app/drf_auth.py rename to packages/runtime-sdk/tests/web-frameworks-test/django/src/django_app/drf_auth.py diff --git a/packages/runtime-sdk/tests/web-frameworks-test/django-async/src/django_app/drf_serializers.py b/packages/runtime-sdk/tests/web-frameworks-test/django/src/django_app/drf_serializers.py similarity index 100% rename from packages/runtime-sdk/tests/web-frameworks-test/django-async/src/django_app/drf_serializers.py rename to packages/runtime-sdk/tests/web-frameworks-test/django/src/django_app/drf_serializers.py diff --git a/packages/runtime-sdk/tests/web-frameworks-test/django-async/src/django_app/drf_throttling.py b/packages/runtime-sdk/tests/web-frameworks-test/django/src/django_app/drf_throttling.py similarity index 100% rename from packages/runtime-sdk/tests/web-frameworks-test/django-async/src/django_app/drf_throttling.py rename to packages/runtime-sdk/tests/web-frameworks-test/django/src/django_app/drf_throttling.py diff --git a/packages/runtime-sdk/tests/web-frameworks-test/django-async/src/django_app/drf_views.py b/packages/runtime-sdk/tests/web-frameworks-test/django/src/django_app/drf_views.py similarity index 100% rename from packages/runtime-sdk/tests/web-frameworks-test/django-async/src/django_app/drf_views.py rename to packages/runtime-sdk/tests/web-frameworks-test/django/src/django_app/drf_views.py diff --git a/packages/runtime-sdk/tests/web-frameworks-test/django-async/src/django_app/forms.py b/packages/runtime-sdk/tests/web-frameworks-test/django/src/django_app/forms.py similarity index 100% rename from packages/runtime-sdk/tests/web-frameworks-test/django-async/src/django_app/forms.py rename to packages/runtime-sdk/tests/web-frameworks-test/django/src/django_app/forms.py diff --git a/packages/runtime-sdk/tests/web-frameworks-test/django-async/src/django_app/middleware.py b/packages/runtime-sdk/tests/web-frameworks-test/django/src/django_app/middleware.py similarity index 54% rename from packages/runtime-sdk/tests/web-frameworks-test/django-async/src/django_app/middleware.py rename to packages/runtime-sdk/tests/web-frameworks-test/django/src/django_app/middleware.py index b547305e..1c8f57d7 100644 --- a/packages/runtime-sdk/tests/web-frameworks-test/django-async/src/django_app/middleware.py +++ b/packages/runtime-sdk/tests/web-frameworks-test/django/src/django_app/middleware.py @@ -1,16 +1,24 @@ from asgiref.sync import iscoroutinefunction, markcoroutinefunction -class CustomAsyncMiddleware: +class CustomMiddleware: async_capable = True - sync_capable = False + sync_capable = True def __init__(self, get_response): self.get_response = get_response if iscoroutinefunction(self.get_response): markcoroutinefunction(self) - async def __call__(self, request): + def __call__(self, request): + if iscoroutinefunction(self): + return self.__acall__(request) + request.custom_middleware_applied = True + response = self.get_response(request) + response["X-Custom-Middleware"] = "applied" + return response + + async def __acall__(self, request): request.custom_middleware_applied = True response = await self.get_response(request) response["X-Custom-Middleware"] = "applied" diff --git a/packages/runtime-sdk/tests/web-frameworks-test/django-async/src/django_app/templatetags/__init__.py b/packages/runtime-sdk/tests/web-frameworks-test/django/src/django_app/templatetags/__init__.py similarity index 100% rename from packages/runtime-sdk/tests/web-frameworks-test/django-async/src/django_app/templatetags/__init__.py rename to packages/runtime-sdk/tests/web-frameworks-test/django/src/django_app/templatetags/__init__.py diff --git a/packages/runtime-sdk/tests/web-frameworks-test/django-async/src/django_app/templatetags/custom_tags.py b/packages/runtime-sdk/tests/web-frameworks-test/django/src/django_app/templatetags/custom_tags.py similarity index 100% rename from packages/runtime-sdk/tests/web-frameworks-test/django-async/src/django_app/templatetags/custom_tags.py rename to packages/runtime-sdk/tests/web-frameworks-test/django/src/django_app/templatetags/custom_tags.py diff --git a/packages/runtime-sdk/tests/web-frameworks-test/django-async/src/django_app/urls.py b/packages/runtime-sdk/tests/web-frameworks-test/django/src/django_app/urls.py similarity index 100% rename from packages/runtime-sdk/tests/web-frameworks-test/django-async/src/django_app/urls.py rename to packages/runtime-sdk/tests/web-frameworks-test/django/src/django_app/urls.py diff --git a/packages/runtime-sdk/tests/web-frameworks-test/django/src/django_app/urls_sync.py b/packages/runtime-sdk/tests/web-frameworks-test/django/src/django_app/urls_sync.py new file mode 100644 index 00000000..e198775d --- /dev/null +++ b/packages/runtime-sdk/tests/web-frameworks-test/django/src/django_app/urls_sync.py @@ -0,0 +1,64 @@ +from django.urls import include, path, re_path +from rest_framework.routers import DefaultRouter + +from django_app import views_sync as views + +router = DefaultRouter() + + +urlpatterns = [ + path("hello/", views.hello, name="hello"), + path("status//", views.status_code, name="status-code"), + path("echo-method/", views.echo_method, name="echo-method"), + path("echo-headers/", views.echo_headers, name="echo-headers"), + path("echo-query/", views.echo_query, name="echo-query"), + path("echo-body/", views.echo_body, name="echo-body"), + path("echo-form/", views.echo_form, name="echo-form"), + path("items//", views.item_detail, name="item-detail"), + path("users//", views.user_detail, name="user-detail"), + path("posts//", views.post_detail, name="post-detail"), + path("uuids//", views.uuid_detail, name="uuid-detail"), + re_path(r"^archive/(?P[0-9]{4})/$", views.archive_year, name="archive-year"), + path("api/v1/", include(("django_app.api_urls", "api"), namespace="v1")), + path("reverse-test/", views.reverse_test, name="reverse-test"), + path("template/hello/", views.template_hello, name="template-hello"), + path("template/context/", views.template_context, name="template-context"), + path( + "template/inheritance/", views.template_inheritance, name="template-inheritance" + ), + path("form/validate/", views.form_validate, name="form-validate"), + path("form/process/", views.form_process, name="form-process"), + path("trigger-404/", views.trigger_404, name="trigger-404"), + path("trigger-403/", views.trigger_403, name="trigger-403"), + path("trigger-400/", views.trigger_400, name="trigger-400"), + path("trigger-500/", views.trigger_500, name="trigger-500"), + path("session/set/", views.session_set, name="session-set"), + path("session/get/", views.session_get, name="session-get"), + path("session/flush/", views.session_flush, name="session-flush"), + path("stream/sync-gen/", views.stream_sync_gen, name="stream-sync-gen"), + path("csrf/form/", views.csrf_form, name="csrf-form"), + path("csrf/exempt/", views.csrf_exempt_view, name="csrf-exempt"), + path("cache/set/", views.cache_set, name="cache-set"), + path("cache/get/", views.cache_get, name="cache-get"), + path("cache/delete/", views.cache_delete, name="cache-delete"), + path("cache/clear/", views.cache_clear, name="cache-clear"), + path("signals/send/", views.signal_send, name="signal-send"), + path("signals/robust/", views.signal_robust, name="signal-robust"), + path("auth/login/", views.auth_login, name="auth-login"), + path("auth/logout/", views.auth_logout, name="auth-logout"), + path("auth/user/", views.auth_user, name="auth-user"), + path("auth/protected/", views.auth_protected, name="auth-protected"), + path("auth/permission/", views.auth_permission, name="auth-permission"), + path("upload/single/", views.upload_single, name="upload-single"), + path("upload/multiple/", views.upload_multiple, name="upload-multiple"), + path("paginate/", views.paginate_view, name="paginate"), + path("cbv/", views.SyncCBV.as_view(), name="async-cbv"), + path("drf/", include(router.urls)), + path("drf/", include("django_app.drf_views")), +] + + +handler400 = "django_app.views_sync.custom_handler400" +handler403 = "django_app.views_sync.custom_handler403" +handler404 = "django_app.views_sync.custom_handler404" +handler500 = "django_app.views_sync.custom_handler500" diff --git a/packages/runtime-sdk/tests/web-frameworks-test/django-async/src/django_app/views.py b/packages/runtime-sdk/tests/web-frameworks-test/django/src/django_app/views.py similarity index 100% rename from packages/runtime-sdk/tests/web-frameworks-test/django-async/src/django_app/views.py rename to packages/runtime-sdk/tests/web-frameworks-test/django/src/django_app/views.py diff --git a/packages/runtime-sdk/tests/web-frameworks-test/django/src/django_app/views_sync.py b/packages/runtime-sdk/tests/web-frameworks-test/django/src/django_app/views_sync.py new file mode 100644 index 00000000..8230ba22 --- /dev/null +++ b/packages/runtime-sdk/tests/web-frameworks-test/django/src/django_app/views_sync.py @@ -0,0 +1,334 @@ +import json + +from django.contrib.auth import authenticate, login, logout +from django.contrib.auth.decorators import login_required, permission_required +from django.core.cache import cache +from django.core.exceptions import PermissionDenied, SuspiciousOperation +from django.core.paginator import Paginator +from django.dispatch import Signal +from django.http import Http404, HttpResponse, JsonResponse, StreamingHttpResponse +from django.shortcuts import render +from django.urls import reverse +from django.utils.html import escape +from django.views import View +from django.views.decorators.csrf import ( + csrf_exempt, + csrf_protect, + ensure_csrf_cookie, +) + +from django_app.forms import ContactForm + +custom_signal = Signal() +signal_events = [] + + +def signal_receiver(sender, **kwargs): + data = kwargs.get("data") + signal_events.append(data) + return data + + +custom_signal.connect(signal_receiver, dispatch_uid="django_app_signal_receiver") + + +def _json_body(request): + if not request.body: + return {} + return json.loads(request.body.decode()) + + +def hello(request): + return HttpResponse("hello") + + +def status_code(request, code): + return HttpResponse(f"status {escape(str(code))}", status=code) + + +def echo_method(request): + return JsonResponse({"method": request.method}) + + +def echo_headers(request): + return JsonResponse({k.lower(): v for k, v in request.headers.items()}) + + +def echo_query(request): + return JsonResponse(request.GET.dict()) + + +def echo_body(request): + return JsonResponse(_json_body(request)) + + +def echo_form(request): + return JsonResponse(request.POST.dict()) + + +def item_detail(request, id): + return JsonResponse({"id": id, "type": type(id).__name__}) + + +def user_detail(request, name): + return JsonResponse({"name": name}) + + +def post_detail(request, slug): + return JsonResponse({"slug": slug}) + + +def uuid_detail(request, uid): + return JsonResponse({"uuid": str(uid)}) + + +def archive_year(request, year): + return JsonResponse({"year": year}) + + +def reverse_test(request): + return HttpResponse(reverse("hello")) + + +def template_hello(request): + return render(request, "hello.html", {"name": "World"}) + + +def template_context(request): + return render( + request, + "context_test.html", + {"items": [1, 2, 3], "show_items": True, "greeting": "hello"}, + ) + + +def template_inheritance(request): + return render(request, "base.html") + + +def form_validate(request): + if request.method == "POST": + form = ContactForm(request.POST) + if form.is_valid(): + return JsonResponse({"valid": True, "data": form.cleaned_data}) + return JsonResponse({"valid": False, "errors": form.errors.get_json_data()}) + form = ContactForm() + return HttpResponse(f"
{form.as_div()}
") + + +def form_process(request): + form = ContactForm(request.POST or None) + valid = form.is_valid() if request.method == "POST" else False + return JsonResponse( + { + "valid": valid, + "data": form.cleaned_data if valid else None, + "errors": None + if valid + else form.errors.get_json_data() + if request.method == "POST" + else None, + } + ) + + +def trigger_404(request): + raise Http404("not found") + + +def trigger_403(request): + raise PermissionDenied("forbidden") + + +def trigger_400(request): + raise SuspiciousOperation("bad request") + + +def trigger_500(request): + raise RuntimeError("server error") + + +def custom_handler400(request, exception): + return JsonResponse({"error": "bad_request", "detail": str(exception)}, status=400) + + +def custom_handler403(request, exception): + return JsonResponse({"error": "forbidden", "detail": str(exception)}, status=403) + + +def custom_handler404(request, exception): + return JsonResponse({"error": "not_found", "detail": str(exception)}, status=404) + + +def custom_handler500(request): + return JsonResponse({"error": "server_error"}, status=500) + + +def session_set(request): + payload = _json_body(request) + for key, value in payload.items(): + request.session[key] = value + return JsonResponse({"set": list(payload.keys())}) + + +def session_get(request): + return JsonResponse(dict(request.session.items())) + + +def session_flush(request): + request.session.flush() + return JsonResponse({"flushed": True}) + + +def _sync_stream(): + for index in range(5): + yield f"chunk-{index}\n" + + +def stream_sync_gen(request): + return StreamingHttpResponse(_sync_stream(), content_type="text/plain") + + +@csrf_protect +@ensure_csrf_cookie +def csrf_form(request): + if request.method == "POST": + return JsonResponse({"csrf": "ok"}) + return render(request, "csrf_form.html") + + +@csrf_exempt +def csrf_exempt_view(request): + return JsonResponse({"csrf_exempt": True}) + + +def cache_set(request): + payload = _json_body(request) + key = payload.get("key") + value = payload.get("value") + cache.set(key, value) + return JsonResponse({"cached": True, "key": key, "value": value}) + + +def cache_get(request): + key = request.GET.get("key") + value = cache.get(key) + return JsonResponse({"key": key, "value": value}) + + +def cache_delete(request): + key = request.GET.get("key") + cache.delete(key) + return JsonResponse({"deleted": True, "key": key}) + + +def cache_clear(request): + cache.clear() + return JsonResponse({"cleared": True}) + + +def signal_send(request): + signal_events.clear() + custom_signal.send(sender=None, data="test") + return JsonResponse({"sent": True, "received": list(signal_events)}) + + +def signal_robust(request): + signal_events.clear() + responses = custom_signal.send_robust(sender=None, data="test") + results = [] + for receiver, response in responses: + results.append( + { + "receiver": getattr(receiver, "__name__", receiver.__class__.__name__), + "response": str(response), + } + ) + return JsonResponse({"sent": True, "results": results}) + + +def auth_login(request): + payload = _json_body(request) + user = authenticate( + request, + username=payload.get("username"), + password=payload.get("password"), + ) + if user is None: + return JsonResponse({"authenticated": False}) + login(request, user, backend="django_app.auth_backend.InMemoryBackend") + return JsonResponse({"authenticated": True}) + + +def auth_logout(request): + logout(request) + return JsonResponse({"logged_out": True}) + + +def auth_user(request): + user = request.user + return JsonResponse( + { + "is_authenticated": user.is_authenticated, + "username": getattr(user, "username", ""), + } + ) + + +@login_required +def auth_protected(request): + user = request.user + return JsonResponse({"protected": True, "user": user.username}) + + +@permission_required("can_view") +def auth_permission(request): + return JsonResponse({"permission": True}) + + +def upload_single(request): + uploaded_file = request.FILES["file"] + return JsonResponse( + { + "name": uploaded_file.name, + "size": uploaded_file.size, + "content": uploaded_file.read().decode(), + } + ) + + +def upload_multiple(request): + files = [{"name": f.name, "size": f.size} for f in request.FILES.getlist("file")] + return JsonResponse({"files": files}) + + +def paginate_view(request): + items = list(range(1, 51)) + paginator = Paginator(items, 10) + page = paginator.get_page(request.GET.get("page", 1)) + return JsonResponse( + { + "page": page.number, + "items": list(page.object_list), + "num_pages": paginator.num_pages, + "has_next": page.has_next(), + "has_previous": page.has_previous(), + } + ) + + +class SyncCBV(View): + def get(self, request): + return JsonResponse({"method": "GET"}) + + def post(self, request): + return JsonResponse({"method": "POST"}) + + def put(self, request): + return JsonResponse({"method": "PUT"}) + + def delete(self, request): + return JsonResponse({"method": "DELETE"}) + + def patch(self, request): + return JsonResponse({"method": "PATCH"}) diff --git a/packages/runtime-sdk/tests/web-frameworks-test/django-async/src/templates/base.html b/packages/runtime-sdk/tests/web-frameworks-test/django/src/templates/base.html similarity index 100% rename from packages/runtime-sdk/tests/web-frameworks-test/django-async/src/templates/base.html rename to packages/runtime-sdk/tests/web-frameworks-test/django/src/templates/base.html diff --git a/packages/runtime-sdk/tests/web-frameworks-test/django-async/src/templates/context_test.html b/packages/runtime-sdk/tests/web-frameworks-test/django/src/templates/context_test.html similarity index 100% rename from packages/runtime-sdk/tests/web-frameworks-test/django-async/src/templates/context_test.html rename to packages/runtime-sdk/tests/web-frameworks-test/django/src/templates/context_test.html diff --git a/packages/runtime-sdk/tests/web-frameworks-test/django-async/src/templates/csrf_form.html b/packages/runtime-sdk/tests/web-frameworks-test/django/src/templates/csrf_form.html similarity index 100% rename from packages/runtime-sdk/tests/web-frameworks-test/django-async/src/templates/csrf_form.html rename to packages/runtime-sdk/tests/web-frameworks-test/django/src/templates/csrf_form.html diff --git a/packages/runtime-sdk/tests/web-frameworks-test/django-async/src/templates/hello.html b/packages/runtime-sdk/tests/web-frameworks-test/django/src/templates/hello.html similarity index 100% rename from packages/runtime-sdk/tests/web-frameworks-test/django-async/src/templates/hello.html rename to packages/runtime-sdk/tests/web-frameworks-test/django/src/templates/hello.html diff --git a/packages/runtime-sdk/tests/web-frameworks-test/django-async/src/test_async_utilities.py b/packages/runtime-sdk/tests/web-frameworks-test/django/src/test_async_utilities.py similarity index 50% rename from packages/runtime-sdk/tests/web-frameworks-test/django-async/src/test_async_utilities.py rename to packages/runtime-sdk/tests/web-frameworks-test/django/src/test_async_utilities.py index a6e9cde1..790c5a58 100644 --- a/packages/runtime-sdk/tests/web-frameworks-test/django-async/src/test_async_utilities.py +++ b/packages/runtime-sdk/tests/web-frameworks-test/django/src/test_async_utilities.py @@ -1,18 +1,20 @@ import pytest from _client import get_json +pytestmark = pytest.mark.asgi_only + @pytest.mark.asyncio -async def test_sync_to_async(django_asgi_app): - response, payload = await get_json(django_asgi_app, "/async/sync-to-async/") +async def test_sync_to_async(django_app): + response, payload = await get_json(django_app, "/async/sync-to-async/") assert response.status == 200 assert payload["result"] == "sync result" @pytest.mark.asyncio -async def test_async_iterator(django_asgi_app): - response, payload = await get_json(django_asgi_app, "/async/async-iter/") +async def test_async_iterator(django_app): + response, payload = await get_json(django_app, "/async/async-iter/") assert response.status == 200 assert payload["items"] diff --git a/packages/runtime-sdk/tests/web-frameworks-test/django-async/src/test_auth.py b/packages/runtime-sdk/tests/web-frameworks-test/django/src/test_auth.py similarity index 62% rename from packages/runtime-sdk/tests/web-frameworks-test/django-async/src/test_auth.py rename to packages/runtime-sdk/tests/web-frameworks-test/django/src/test_auth.py index 9bc959c6..a31abff0 100644 --- a/packages/runtime-sdk/tests/web-frameworks-test/django-async/src/test_auth.py +++ b/packages/runtime-sdk/tests/web-frameworks-test/django/src/test_auth.py @@ -2,9 +2,9 @@ from _client import cookie_value, fetch, get_json, post_json, read_json -async def _login(django_asgi_app, password="testpass123"): +async def _login(django_app, password="testpass123"): response = await post_json( - django_asgi_app, + django_app, "/auth/login/", {"username": "testuser", "password": password}, ) @@ -14,36 +14,36 @@ async def _login(django_asgi_app, password="testpass123"): @pytest.mark.asyncio -async def test_auth_login_success(django_asgi_app): - response, payload, _ = await _login(django_asgi_app) +async def test_auth_login_success(django_app): + response, payload, _ = await _login(django_app) assert response.status == 200 assert payload["authenticated"] is True @pytest.mark.asyncio -async def test_auth_login_failure(django_asgi_app): - response, payload, _ = await _login(django_asgi_app, password="wrong-password") +async def test_auth_login_failure(django_app): + response, payload, _ = await _login(django_app, password="wrong-password") assert response.status == 200 assert payload["authenticated"] is False @pytest.mark.asyncio -async def test_auth_user_anonymous(django_asgi_app): - response, payload = await get_json(django_asgi_app, "/auth/user/") +async def test_auth_user_anonymous(django_app): + response, payload = await get_json(django_app, "/auth/user/") assert response.status == 200 assert payload["is_authenticated"] is False @pytest.mark.asyncio -async def test_auth_user_after_login(django_asgi_app): - _, _, sessionid = await _login(django_asgi_app) +async def test_auth_user_after_login(django_app): + _, _, sessionid = await _login(django_app) assert sessionid is not None response, payload = await get_json( - django_asgi_app, "/auth/user/", headers={"Cookie": f"sessionid={sessionid}"} + django_app, "/auth/user/", headers={"Cookie": f"sessionid={sessionid}"} ) assert response.status == 200 @@ -52,12 +52,12 @@ async def test_auth_user_after_login(django_asgi_app): @pytest.mark.asyncio -async def test_auth_logout(django_asgi_app): - _, _, sessionid = await _login(django_asgi_app) +async def test_auth_logout(django_app): + _, _, sessionid = await _login(django_app) assert sessionid is not None logout_response = await post_json( - django_asgi_app, + django_app, "/auth/logout/", {}, headers={"Cookie": f"sessionid={sessionid}"}, @@ -69,16 +69,16 @@ async def test_auth_logout(django_asgi_app): @pytest.mark.asyncio -async def test_auth_protected_access(django_asgi_app): - _, _, sessionid = await _login(django_asgi_app) +async def test_auth_protected_access(django_app): + _, _, sessionid = await _login(django_app) assert sessionid is not None - anon = await fetch(django_asgi_app, "/auth/protected/") + anon = await fetch(django_app, "/auth/protected/") assert anon.status == 302 assert anon.headers.get("Location") is not None response, payload = await get_json( - django_asgi_app, + django_app, "/auth/protected/", headers={"Cookie": f"sessionid={sessionid}"}, ) diff --git a/packages/runtime-sdk/tests/web-frameworks-test/django/src/test_caching.py b/packages/runtime-sdk/tests/web-frameworks-test/django/src/test_caching.py new file mode 100644 index 00000000..6b1f890d --- /dev/null +++ b/packages/runtime-sdk/tests/web-frameworks-test/django/src/test_caching.py @@ -0,0 +1,60 @@ +import pytest +from _client import get_json, post_json, read_json + + +@pytest.mark.asyncio +async def test_cache_set_and_get(django_app): + response = await post_json(django_app, "/cache/set/", {"key": "k1", "value": "v1"}) + assert response.status == 200 + assert await read_json(response) == {"cached": True, "key": "k1", "value": "v1"} + + response, payload = await get_json(django_app, "/cache/get/?key=k1") + assert response.status == 200 + assert payload == {"key": "k1", "value": "v1"} + + +@pytest.mark.asyncio +async def test_cache_delete_and_missing(django_app): + await post_json( + django_app, "/cache/set/", {"key": "delete-key", "value": "delete-value"} + ) + response, payload = await get_json(django_app, "/cache/delete/?key=delete-key") + assert response.status == 200 + assert payload == {"deleted": True, "key": "delete-key"} + + response, payload = await get_json(django_app, "/cache/get/?key=delete-key") + assert response.status == 200 + assert payload == {"key": "delete-key", "value": None} + + response, payload = await get_json(django_app, "/cache/get/?key=nonexistent") + assert response.status == 200 + assert payload == {"key": "nonexistent", "value": None} + + +@pytest.mark.asyncio +async def test_cache_clear(django_app): + for key, value in (("clear-key-1", "value-1"), ("clear-key-2", "value-2")): + await post_json(django_app, "/cache/set/", {"key": key, "value": value}) + + response, payload = await get_json(django_app, "/cache/clear/") + assert response.status == 200 + assert payload["cleared"] is True + + for key in ("clear-key-1", "clear-key-2"): + response, payload = await get_json(django_app, f"/cache/get/?key={key}") + assert response.status == 200 + assert payload == {"key": key, "value": None} + + +@pytest.mark.asyncio +async def test_cache_overwrite(django_app): + await post_json( + django_app, "/cache/set/", {"key": "overwrite-key", "value": "value-1"} + ) + await post_json( + django_app, "/cache/set/", {"key": "overwrite-key", "value": "value-2"} + ) + + response, payload = await get_json(django_app, "/cache/get/?key=overwrite-key") + assert response.status == 200 + assert payload == {"key": "overwrite-key", "value": "value-2"} diff --git a/packages/runtime-sdk/tests/web-frameworks-test/django-async/src/test_cors.py b/packages/runtime-sdk/tests/web-frameworks-test/django/src/test_cors.py similarity index 75% rename from packages/runtime-sdk/tests/web-frameworks-test/django-async/src/test_cors.py rename to packages/runtime-sdk/tests/web-frameworks-test/django/src/test_cors.py index 82b9c20d..6c9f7a6b 100644 --- a/packages/runtime-sdk/tests/web-frameworks-test/django-async/src/test_cors.py +++ b/packages/runtime-sdk/tests/web-frameworks-test/django/src/test_cors.py @@ -6,7 +6,7 @@ @pytest.mark.asyncio -async def test_cors_allowed_origins(django_asgi_app): +async def test_cors_allowed_origins(django_app): cases = ( ("OPTIONS", "http://localhost:3000", True), ("GET", "https://example.com", False), @@ -17,9 +17,7 @@ async def test_cors_allowed_origins(django_asgi_app): headers = {"Origin": origin} if method == "OPTIONS": headers["Access-Control-Request-Method"] = "GET" - response = await fetch( - django_asgi_app, "/hello/", method=method, headers=headers - ) + response = await fetch(django_app, "/hello/", method=method, headers=headers) assert response.headers.get(ORIGIN_HEADER) == origin if with_credentials: @@ -27,9 +25,9 @@ async def test_cors_allowed_origins(django_asgi_app): @pytest.mark.asyncio -async def test_cors_disallowed_origin(django_asgi_app): +async def test_cors_disallowed_origin(django_app): response = await fetch( - django_asgi_app, + django_app, "/hello/", method="OPTIONS", headers={"Origin": "http://evil.com", "Access-Control-Request-Method": "GET"}, @@ -39,8 +37,8 @@ async def test_cors_disallowed_origin(django_asgi_app): @pytest.mark.asyncio -async def test_cors_no_origin_no_headers(django_asgi_app): - response = await fetch(django_asgi_app, "/hello/") +async def test_cors_no_origin_no_headers(django_app): + response = await fetch(django_app, "/hello/") assert not response.headers.has(ORIGIN_HEADER) assert not response.headers.has(CREDENTIALS_HEADER) diff --git a/packages/runtime-sdk/tests/web-frameworks-test/django-async/src/test_csrf.py b/packages/runtime-sdk/tests/web-frameworks-test/django/src/test_csrf.py similarity index 76% rename from packages/runtime-sdk/tests/web-frameworks-test/django-async/src/test_csrf.py rename to packages/runtime-sdk/tests/web-frameworks-test/django/src/test_csrf.py index bd56d2f7..11876c7f 100644 --- a/packages/runtime-sdk/tests/web-frameworks-test/django-async/src/test_csrf.py +++ b/packages/runtime-sdk/tests/web-frameworks-test/django/src/test_csrf.py @@ -5,8 +5,8 @@ @pytest.mark.asyncio -async def test_csrf_form_renders_token(django_asgi_app): - response = await fetch(django_asgi_app, "/csrf/form/") +async def test_csrf_form_renders_token(django_app): + response = await fetch(django_app, "/csrf/form/") body = await response.text() cookie_token = cookie_value(response.headers.get("Set-Cookie"), "csrftoken") @@ -16,9 +16,9 @@ async def test_csrf_form_renders_token(django_asgi_app): @pytest.mark.asyncio -async def test_post_without_csrf_rejected(django_asgi_app): +async def test_post_without_csrf_rejected(django_app): response = await fetch( - django_asgi_app, + django_app, "/csrf/form/", method="POST", headers={"Content-Type": "application/x-www-form-urlencoded"}, @@ -29,16 +29,16 @@ async def test_post_without_csrf_rejected(django_asgi_app): @pytest.mark.asyncio -async def test_csrf_exempt_allows_post(django_asgi_app): - response = await post_json(django_asgi_app, "/csrf/exempt/", {"value": "ok"}) +async def test_csrf_exempt_allows_post(django_app): + response = await post_json(django_app, "/csrf/exempt/", {"value": "ok"}) assert response.status == 200 assert await read_json(response) == {"csrf_exempt": True} @pytest.mark.asyncio -async def test_csrf_form_token_post(django_asgi_app): - get_response = await fetch(django_asgi_app, "/csrf/form/") +async def test_csrf_form_token_post(django_app): + get_response = await fetch(django_app, "/csrf/form/") body = await get_response.text() form_token = None for pattern in ( @@ -55,7 +55,7 @@ async def test_csrf_form_token_post(django_asgi_app): assert cookie_token is not None post_response = await fetch( - django_asgi_app, + django_app, "/csrf/form/", method="POST", headers={ diff --git a/packages/runtime-sdk/tests/web-frameworks-test/django-async/src/test_drf_auth_permissions.py b/packages/runtime-sdk/tests/web-frameworks-test/django/src/test_drf_auth_permissions.py similarity index 84% rename from packages/runtime-sdk/tests/web-frameworks-test/django-async/src/test_drf_auth_permissions.py rename to packages/runtime-sdk/tests/web-frameworks-test/django/src/test_drf_auth_permissions.py index c1bbd9f4..536c6bf8 100644 --- a/packages/runtime-sdk/tests/web-frameworks-test/django-async/src/test_drf_auth_permissions.py +++ b/packages/runtime-sdk/tests/web-frameworks-test/django/src/test_drf_auth_permissions.py @@ -3,7 +3,7 @@ @pytest.mark.asyncio -async def test_auth_and_permission_cases(django_asgi_app): +async def test_auth_and_permission_cases(django_app): cases = [ ("/drf/auth/", None, {401, 403}, None), ("/drf/public/", None, 200, {"public": True}), @@ -36,12 +36,12 @@ async def test_auth_and_permission_cases(django_asgi_app): for path, headers, status, expected in cases: if expected is None: - response = await fetch(django_asgi_app, path, headers=headers) + response = await fetch(django_app, path, headers=headers) if isinstance(status, set): assert response.status in status else: assert response.status == status else: - response, payload = await get_json(django_asgi_app, path, headers=headers) + response, payload = await get_json(django_app, path, headers=headers) assert response.status == status assert payload == expected diff --git a/packages/runtime-sdk/tests/web-frameworks-test/django-async/src/test_drf_content_negotiation.py b/packages/runtime-sdk/tests/web-frameworks-test/django/src/test_drf_content_negotiation.py similarity index 79% rename from packages/runtime-sdk/tests/web-frameworks-test/django-async/src/test_drf_content_negotiation.py rename to packages/runtime-sdk/tests/web-frameworks-test/django/src/test_drf_content_negotiation.py index 2cf7d620..991df88f 100644 --- a/packages/runtime-sdk/tests/web-frameworks-test/django-async/src/test_drf_content_negotiation.py +++ b/packages/runtime-sdk/tests/web-frameworks-test/django/src/test_drf_content_negotiation.py @@ -5,9 +5,9 @@ @pytest.mark.asyncio -async def test_content_negotiation_cases(django_asgi_app): +async def test_content_negotiation_cases(django_app): json_data = {"name": "widget", "quantity": 2} - response = await post_json(django_asgi_app, "/drf/content/", json_data) + response = await post_json(django_app, "/drf/content/", json_data) payload = json.loads(await response.text()) assert response.status == 200 @@ -15,7 +15,7 @@ async def test_content_negotiation_cases(django_asgi_app): assert "application/json" in (response.headers.get("Content-Type") or "") response = await post_form( - django_asgi_app, + django_app, "/drf/content/", "name=widget&quantity=2", ) @@ -26,13 +26,13 @@ async def test_content_negotiation_cases(django_asgi_app): assert payload["data"]["name"] == "widget" assert payload["data"]["quantity"] == "2" - response = await post_json(django_asgi_app, "/drf/content/", {"ping": "pong"}) + response = await post_json(django_app, "/drf/content/", {"ping": "pong"}) assert response.status == 200 assert "application/json" in (response.headers.get("Content-Type") or "") response = await post_form( - django_asgi_app, + django_app, "/drf/content/", "", headers={"Content-Type": "text/xml"}, diff --git a/packages/runtime-sdk/tests/web-frameworks-test/django-async/src/test_drf_serializers.py b/packages/runtime-sdk/tests/web-frameworks-test/django/src/test_drf_serializers.py similarity index 87% rename from packages/runtime-sdk/tests/web-frameworks-test/django-async/src/test_drf_serializers.py rename to packages/runtime-sdk/tests/web-frameworks-test/django/src/test_drf_serializers.py index 50419b8a..b636e4e8 100644 --- a/packages/runtime-sdk/tests/web-frameworks-test/django-async/src/test_drf_serializers.py +++ b/packages/runtime-sdk/tests/web-frameworks-test/django/src/test_drf_serializers.py @@ -5,7 +5,7 @@ @pytest.mark.asyncio -async def test_serializer_and_nested_cases(django_asgi_app): +async def test_serializer_and_nested_cases(django_app): serializer_cases = [ ( {"name": "widget", "price": 9.99, "quantity": 3}, @@ -35,12 +35,12 @@ async def test_serializer_and_nested_cases(django_asgi_app): ] for data, valid, check in serializer_cases: - response = await post_json(django_asgi_app, "/drf/serializer/", data) + response = await post_json(django_app, "/drf/serializer/", data) payload = json.loads(await response.text()) assert payload["valid"] is valid assert check(payload) - response, payload = await get_json(django_asgi_app, "/drf/serializer/") + response, payload = await get_json(django_app, "/drf/serializer/") assert response.status == 200 assert "name" in payload assert "price" in payload @@ -62,7 +62,7 @@ async def test_serializer_and_nested_cases(django_asgi_app): ] for data, valid, check in nested_cases: - response = await post_json(django_asgi_app, "/drf/nested/", data) + response = await post_json(django_app, "/drf/nested/", data) payload = json.loads(await response.text()) assert payload["valid"] is valid assert check(payload) diff --git a/packages/runtime-sdk/tests/web-frameworks-test/django-async/src/test_drf_throttling.py b/packages/runtime-sdk/tests/web-frameworks-test/django/src/test_drf_throttling.py similarity index 63% rename from packages/runtime-sdk/tests/web-frameworks-test/django-async/src/test_drf_throttling.py rename to packages/runtime-sdk/tests/web-frameworks-test/django/src/test_drf_throttling.py index 6b3f1d2c..2b0bc746 100644 --- a/packages/runtime-sdk/tests/web-frameworks-test/django-async/src/test_drf_throttling.py +++ b/packages/runtime-sdk/tests/web-frameworks-test/django/src/test_drf_throttling.py @@ -5,9 +5,9 @@ @pytest.mark.asyncio -async def test_throttle_behavior(django_asgi_app): - await fetch(django_asgi_app, "/cache/clear/") - responses = [await fetch(django_asgi_app, "/drf/throttled/") for _ in range(4)] +async def test_throttle_behavior(django_app): + await fetch(django_app, "/cache/clear/") + responses = [await fetch(django_app, "/drf/throttled/") for _ in range(4)] payload = json.loads(await responses[3].text()) assert [response.status for response in responses[:3]] == [200, 200, 200] diff --git a/packages/runtime-sdk/tests/web-frameworks-test/django-async/src/test_drf_versioning.py b/packages/runtime-sdk/tests/web-frameworks-test/django/src/test_drf_versioning.py similarity index 85% rename from packages/runtime-sdk/tests/web-frameworks-test/django-async/src/test_drf_versioning.py rename to packages/runtime-sdk/tests/web-frameworks-test/django/src/test_drf_versioning.py index b67c7688..e6518782 100644 --- a/packages/runtime-sdk/tests/web-frameworks-test/django-async/src/test_drf_versioning.py +++ b/packages/runtime-sdk/tests/web-frameworks-test/django/src/test_drf_versioning.py @@ -3,7 +3,7 @@ @pytest.mark.asyncio -async def test_versioning_cases(django_asgi_app): +async def test_versioning_cases(django_app): cases = [ ("/drf/versioned/?version=2.0", None, "2.0"), ("/drf/versioned/", None, None), @@ -12,7 +12,7 @@ async def test_versioning_cases(django_asgi_app): ] for path, headers, expected_version in cases: - response, payload = await get_json(django_asgi_app, path, headers=headers) + response, payload = await get_json(django_app, path, headers=headers) assert response.status == 200 if expected_version is not None: assert payload["version"] == expected_version diff --git a/packages/runtime-sdk/tests/web-frameworks-test/django-async/src/test_drf_views.py b/packages/runtime-sdk/tests/web-frameworks-test/django/src/test_drf_views.py similarity index 80% rename from packages/runtime-sdk/tests/web-frameworks-test/django-async/src/test_drf_views.py rename to packages/runtime-sdk/tests/web-frameworks-test/django/src/test_drf_views.py index 24eb6178..cc9ea167 100644 --- a/packages/runtime-sdk/tests/web-frameworks-test/django-async/src/test_drf_views.py +++ b/packages/runtime-sdk/tests/web-frameworks-test/django/src/test_drf_views.py @@ -5,7 +5,7 @@ @pytest.mark.asyncio -async def test_api_view_and_function_view(django_asgi_app): +async def test_api_view_and_function_view(django_app): cases = [ ("GET", "/drf/api-view/", None, 200, {"method": "GET", "query": {}}), ( @@ -42,13 +42,13 @@ async def test_api_view_and_function_view(django_asgi_app): for method, path, data, status, expected in cases: if method == "GET": - response, payload = await get_json(django_asgi_app, path) + response, payload = await get_json(django_app, path) elif method == "DELETE": - response = await fetch(django_asgi_app, path, method=method) + response = await fetch(django_app, path, method=method) payload = None elif method == "PUT": response = await fetch( - django_asgi_app, + django_app, path, method=method, headers={"Content-Type": "application/json"}, @@ -56,7 +56,7 @@ async def test_api_view_and_function_view(django_asgi_app): ) payload = json.loads(await response.text()) else: - response = await post_json(django_asgi_app, path, data) + response = await post_json(django_app, path, data) payload = json.loads(await response.text()) assert response.status == status if expected is not None: @@ -67,7 +67,7 @@ async def test_api_view_and_function_view(django_asgi_app): @pytest.mark.asyncio -async def test_method_not_allowed(django_asgi_app): - response = await fetch(django_asgi_app, "/drf/function-view/", method="PATCH") +async def test_method_not_allowed(django_app): + response = await fetch(django_app, "/drf/function-view/", method="PATCH") assert response.status == 405 diff --git a/packages/runtime-sdk/tests/web-frameworks-test/django-async/src/test_error_handling.py b/packages/runtime-sdk/tests/web-frameworks-test/django/src/test_error_handling.py similarity index 60% rename from packages/runtime-sdk/tests/web-frameworks-test/django-async/src/test_error_handling.py rename to packages/runtime-sdk/tests/web-frameworks-test/django/src/test_error_handling.py index b2ecae71..9452e214 100644 --- a/packages/runtime-sdk/tests/web-frameworks-test/django-async/src/test_error_handling.py +++ b/packages/runtime-sdk/tests/web-frameworks-test/django/src/test_error_handling.py @@ -3,14 +3,14 @@ @pytest.mark.asyncio -async def test_error_handlers(django_asgi_app): +async def test_error_handlers(django_app): for path, status in ( ("/trigger-404/", 404), ("/trigger-403/", 403), ("/trigger-400/", 400), ("/trigger-500/", 500), ): - response, payload = await get_json(django_asgi_app, path) + response, payload = await get_json(django_app, path) assert response.status == status assert isinstance(payload, dict) @@ -18,15 +18,15 @@ async def test_error_handlers(django_asgi_app): @pytest.mark.asyncio -async def test_unmatched_404(django_asgi_app): - response = await fetch(django_asgi_app, "/nonexistent-path-xyz/") +async def test_unmatched_404(django_app): + response = await fetch(django_app, "/nonexistent-path-xyz/") assert response.status == 404 @pytest.mark.asyncio -async def test_error_response_is_json(django_asgi_app): - response = await fetch(django_asgi_app, "/trigger-404/") +async def test_error_response_is_json(django_app): + response = await fetch(django_app, "/trigger-404/") assert response.status == 404 assert "application/json" in (response.headers.get("Content-Type") or "") diff --git a/packages/runtime-sdk/tests/web-frameworks-test/django-async/src/test_file_uploads.py b/packages/runtime-sdk/tests/web-frameworks-test/django/src/test_file_uploads.py similarity index 90% rename from packages/runtime-sdk/tests/web-frameworks-test/django-async/src/test_file_uploads.py rename to packages/runtime-sdk/tests/web-frameworks-test/django/src/test_file_uploads.py index 79d69e91..e7b1250d 100644 --- a/packages/runtime-sdk/tests/web-frameworks-test/django-async/src/test_file_uploads.py +++ b/packages/runtime-sdk/tests/web-frameworks-test/django/src/test_file_uploads.py @@ -13,7 +13,7 @@ def _normalize_files_payload(payload): @pytest.mark.asyncio -async def test_single_file_upload(django_asgi_app): +async def test_single_file_upload(django_app): for filename, content, expected in ( ("test.txt", "hello", {"name": "test.txt", "content": "hello", "size": 5}), ( @@ -25,7 +25,7 @@ async def test_single_file_upload(django_asgi_app): ): body, content_type = build_multipart([("file", filename, content)]) response, payload = await get_json( - django_asgi_app, + django_app, "/upload/single/", method="POST", headers={"Content-Type": content_type}, @@ -38,12 +38,12 @@ async def test_single_file_upload(django_asgi_app): @pytest.mark.asyncio -async def test_multiple_file_upload(django_asgi_app): +async def test_multiple_file_upload(django_app): body, content_type = build_multipart( [("file", "one.txt", "one"), ("file", "two.txt", "two")] ) response, payload = await get_json( - django_asgi_app, + django_app, "/upload/multiple/", method="POST", headers={"Content-Type": content_type}, diff --git a/packages/runtime-sdk/tests/web-frameworks-test/django-async/src/test_forms.py b/packages/runtime-sdk/tests/web-frameworks-test/django/src/test_forms.py similarity index 85% rename from packages/runtime-sdk/tests/web-frameworks-test/django-async/src/test_forms.py rename to packages/runtime-sdk/tests/web-frameworks-test/django/src/test_forms.py index 9c47ab74..a7f6fdd0 100644 --- a/packages/runtime-sdk/tests/web-frameworks-test/django-async/src/test_forms.py +++ b/packages/runtime-sdk/tests/web-frameworks-test/django/src/test_forms.py @@ -5,7 +5,7 @@ @pytest.mark.asyncio -async def test_form_validation(django_asgi_app): +async def test_form_validation(django_app): for label, body in ( ( "valid", @@ -19,7 +19,7 @@ async def test_form_validation(django_asgi_app): ), ("cross_field", "name=alice&email=alice%40example.com&age=17&category=support"), ): - response = await post_form(django_asgi_app, "/form/validate/", body) + response = await post_form(django_app, "/form/validate/", body) assert response.status == 200 payload = json.loads(await response.text()) @@ -41,8 +41,8 @@ async def test_form_validation(django_asgi_app): @pytest.mark.asyncio -async def test_form_get_renders_html(django_asgi_app): - response = await fetch(django_asgi_app, "/form/validate/") +async def test_form_get_renders_html(django_app): + response = await fetch(django_app, "/form/validate/") assert response.status == 200 body = await response.text() diff --git a/packages/runtime-sdk/tests/web-frameworks-test/django-async/src/test_middleware.py b/packages/runtime-sdk/tests/web-frameworks-test/django/src/test_middleware.py similarity index 67% rename from packages/runtime-sdk/tests/web-frameworks-test/django-async/src/test_middleware.py rename to packages/runtime-sdk/tests/web-frameworks-test/django/src/test_middleware.py index 49dd1af4..d0bbe4ad 100644 --- a/packages/runtime-sdk/tests/web-frameworks-test/django-async/src/test_middleware.py +++ b/packages/runtime-sdk/tests/web-frameworks-test/django/src/test_middleware.py @@ -3,14 +3,14 @@ @pytest.mark.asyncio -async def test_middleware_headers(django_asgi_app): +async def test_middleware_headers(django_app): for path in ("/hello/", "/echo-method/"): - response = await fetch(django_asgi_app, path) + response = await fetch(django_app, path) assert response.status == 200 assert response.headers.get("X-Custom-Middleware") == "applied" - response = await fetch(django_asgi_app, "/hello/") + response = await fetch(django_app, "/hello/") assert response.status == 200 assert response.headers.get("X-Content-Type-Options") == "nosniff" @@ -20,8 +20,8 @@ async def test_middleware_headers(django_asgi_app): @pytest.mark.asyncio -async def test_session_middleware_cookie(django_asgi_app): - response = await post_json(django_asgi_app, "/session/set/", {"color": "blue"}) +async def test_session_middleware_cookie(django_app): + response = await post_json(django_app, "/session/set/", {"color": "blue"}) assert response.status == 200 assert response.headers.get("Set-Cookie") is not None diff --git a/packages/runtime-sdk/tests/web-frameworks-test/django-async/src/test_pagination.py b/packages/runtime-sdk/tests/web-frameworks-test/django/src/test_pagination.py similarity index 64% rename from packages/runtime-sdk/tests/web-frameworks-test/django-async/src/test_pagination.py rename to packages/runtime-sdk/tests/web-frameworks-test/django/src/test_pagination.py index dba6657a..5ddd1fcc 100644 --- a/packages/runtime-sdk/tests/web-frameworks-test/django-async/src/test_pagination.py +++ b/packages/runtime-sdk/tests/web-frameworks-test/django/src/test_pagination.py @@ -3,13 +3,13 @@ @pytest.mark.asyncio -async def test_paginator_basic_pages(django_asgi_app): +async def test_paginator_basic_pages(django_app): for page, expected in ( (1, {"has_next": True, "has_previous": False}), (3, {"has_next": True, "has_previous": True}), (5, {"has_next": False, "has_previous": True}), ): - response, payload = await get_json(django_asgi_app, f"/paginate/?page={page}") + response, payload = await get_json(django_app, f"/paginate/?page={page}") assert response.status == 200 assert len(payload["items"]) == 10 @@ -19,8 +19,8 @@ async def test_paginator_basic_pages(django_asgi_app): @pytest.mark.asyncio -async def test_paginator_invalid_page(django_asgi_app): - response, payload = await get_json(django_asgi_app, "/paginate/?page=abc") +async def test_paginator_invalid_page(django_app): + response, payload = await get_json(django_app, "/paginate/?page=abc") assert response.status == 200 assert payload["page"] == 1 @@ -28,8 +28,8 @@ async def test_paginator_invalid_page(django_asgi_app): @pytest.mark.asyncio -async def test_paginator_out_of_range(django_asgi_app): - response, payload = await get_json(django_asgi_app, "/paginate/?page=999") +async def test_paginator_out_of_range(django_app): + response, payload = await get_json(django_app, "/paginate/?page=999") assert response.status == 200 assert payload["page"] == payload["num_pages"] == 5 diff --git a/packages/runtime-sdk/tests/web-frameworks-test/django-async/src/test_request_response.py b/packages/runtime-sdk/tests/web-frameworks-test/django/src/test_request_response.py similarity index 68% rename from packages/runtime-sdk/tests/web-frameworks-test/django-async/src/test_request_response.py rename to packages/runtime-sdk/tests/web-frameworks-test/django/src/test_request_response.py index 8d3ac1d0..6b287fb6 100644 --- a/packages/runtime-sdk/tests/web-frameworks-test/django-async/src/test_request_response.py +++ b/packages/runtime-sdk/tests/web-frameworks-test/django/src/test_request_response.py @@ -5,31 +5,31 @@ @pytest.mark.asyncio -async def test_request_method(django_asgi_app): - response, payload = await get_json(django_asgi_app, "/echo-method/") +async def test_request_method(django_app): + response, payload = await get_json(django_app, "/echo-method/") assert response.status == 200 assert payload["method"] == "GET" @pytest.mark.asyncio -async def test_request_get_params(django_asgi_app): +async def test_request_get_params(django_app): for path, payload in ( ("/echo-query/?key=value", {"key": "value"}), ("/echo-query/?a=1&b=2&c=3", {"a": "1", "b": "2", "c": "3"}), ): - response, data = await get_json(django_asgi_app, path) + response, data = await get_json(django_app, path) assert response.status == 200 assert data == payload @pytest.mark.asyncio -async def test_request_post_bodies(django_asgi_app): +async def test_request_post_bodies(django_app): for make_request, payload in ( ( lambda: post_form( - django_asgi_app, + django_app, "/echo-form/", "name=alice&role=worker", ), @@ -37,7 +37,7 @@ async def test_request_post_bodies(django_asgi_app): ), ( lambda: post_json( - django_asgi_app, + django_app, "/echo-body/", {"name": "alice", "active": True}, ), @@ -51,9 +51,9 @@ async def test_request_post_bodies(django_asgi_app): @pytest.mark.asyncio -async def test_request_headers(django_asgi_app): +async def test_request_headers(django_app): response, payload = await get_json( - django_asgi_app, + django_app, "/echo-headers/", headers={"X-Test-Header": "worker", "X-Trace-Id": "123"}, ) @@ -65,8 +65,8 @@ async def test_request_headers(django_asgi_app): @pytest.mark.asyncio -async def test_request_path_and_content_type(django_asgi_app): - response, payload = await get_json(django_asgi_app, "/echo-method/") +async def test_request_path_and_content_type(django_app): + response, payload = await get_json(django_app, "/echo-method/") assert response.status == 200 assert payload["method"] == "GET" @@ -74,16 +74,16 @@ async def test_request_path_and_content_type(django_asgi_app): @pytest.mark.asyncio -async def test_response_statuses(django_asgi_app): +async def test_response_statuses(django_app): for path, status in (("/status/201/", 201), ("/status/302/", 302)): - response = await fetch(django_asgi_app, path) + response = await fetch(django_app, path) assert response.status == status @pytest.mark.asyncio -async def test_json_response_dict(django_asgi_app): - response, payload = await get_json(django_asgi_app, "/echo-query/?a=1") +async def test_json_response_dict(django_app): + response, payload = await get_json(django_app, "/echo-query/?a=1") assert response.status == 200 assert isinstance(payload, dict) @@ -91,8 +91,8 @@ async def test_json_response_dict(django_asgi_app): @pytest.mark.asyncio -async def test_response_custom_headers(django_asgi_app): - response = await fetch(django_asgi_app, "/hello/") +async def test_response_custom_headers(django_app): + response = await fetch(django_app, "/hello/") assert response.status == 200 assert response.headers.get("X-Custom-Middleware") == "applied" diff --git a/packages/runtime-sdk/tests/web-frameworks-test/django-async/src/test_sessions.py b/packages/runtime-sdk/tests/web-frameworks-test/django/src/test_sessions.py similarity index 70% rename from packages/runtime-sdk/tests/web-frameworks-test/django-async/src/test_sessions.py rename to packages/runtime-sdk/tests/web-frameworks-test/django/src/test_sessions.py index 283040f4..975c9770 100644 --- a/packages/runtime-sdk/tests/web-frameworks-test/django-async/src/test_sessions.py +++ b/packages/runtime-sdk/tests/web-frameworks-test/django/src/test_sessions.py @@ -3,16 +3,16 @@ @pytest.mark.asyncio -async def test_session_set_and_get_values(django_asgi_app): +async def test_session_set_and_get_values(django_app): for data in ({"color": "blue"}, {"color": "blue", "size": "large"}): - set_response = await post_json(django_asgi_app, "/session/set/", data) + set_response = await post_json(django_app, "/session/set/", data) assert set_response.status == 200 cookie = cookie_value(set_response.headers.get("Set-Cookie"), "sessionid") assert cookie get_response, payload = await get_json( - django_asgi_app, + django_app, "/session/get/", headers={"Cookie": f"sessionid={cookie}"}, ) @@ -22,14 +22,14 @@ async def test_session_set_and_get_values(django_asgi_app): @pytest.mark.asyncio -async def test_session_flush(django_asgi_app): - set_response = await post_json(django_asgi_app, "/session/set/", {"color": "blue"}) +async def test_session_flush(django_app): + set_response = await post_json(django_app, "/session/set/", {"color": "blue"}) assert set_response.status == 200 cookie = cookie_value(set_response.headers.get("Set-Cookie"), "sessionid") flush_response = await fetch( - django_asgi_app, + django_app, "/session/flush/", method="POST", headers={"Cookie": f"sessionid={cookie}"}, @@ -39,7 +39,7 @@ async def test_session_flush(django_asgi_app): flushed_cookie = cookie_value(flush_response.headers.get("Set-Cookie"), "sessionid") get_response, payload = await get_json( - django_asgi_app, + django_app, "/session/get/", headers={"Cookie": f"sessionid={flushed_cookie or ''}"}, ) @@ -49,16 +49,14 @@ async def test_session_flush(django_asgi_app): @pytest.mark.asyncio -async def test_session_overwrites(django_asgi_app): - first_response = await post_json( - django_asgi_app, "/session/set/", {"color": "blue"} - ) +async def test_session_overwrites(django_app): + first_response = await post_json(django_app, "/session/set/", {"color": "blue"}) assert first_response.status == 200 cookie = cookie_value(first_response.headers.get("Set-Cookie"), "sessionid") second_response = await post_json( - django_asgi_app, + django_app, "/session/set/", {"color": "red"}, headers={"Cookie": f"sessionid={cookie}"}, @@ -70,7 +68,7 @@ async def test_session_overwrites(django_asgi_app): ) get_response, payload = await get_json( - django_asgi_app, + django_app, "/session/get/", headers={"Cookie": f"sessionid={updated_cookie}"}, ) @@ -80,16 +78,16 @@ async def test_session_overwrites(django_asgi_app): @pytest.mark.asyncio -async def test_session_cookie_present(django_asgi_app): - response = await post_json(django_asgi_app, "/session/set/", {"color": "blue"}) +async def test_session_cookie_present(django_app): + response = await post_json(django_app, "/session/set/", {"color": "blue"}) assert response.status == 200 assert cookie_value(response.headers.get("Set-Cookie"), "sessionid") @pytest.mark.asyncio -async def test_session_empty_initially(django_asgi_app): - response, payload = await get_json(django_asgi_app, "/session/get/") +async def test_session_empty_initially(django_app): + response, payload = await get_json(django_app, "/session/get/") assert response.status == 200 assert payload == {} diff --git a/packages/runtime-sdk/tests/web-frameworks-test/django-async/src/test_signals.py b/packages/runtime-sdk/tests/web-frameworks-test/django/src/test_signals.py similarity index 53% rename from packages/runtime-sdk/tests/web-frameworks-test/django-async/src/test_signals.py rename to packages/runtime-sdk/tests/web-frameworks-test/django/src/test_signals.py index 53d7f8aa..b44ee702 100644 --- a/packages/runtime-sdk/tests/web-frameworks-test/django-async/src/test_signals.py +++ b/packages/runtime-sdk/tests/web-frameworks-test/django/src/test_signals.py @@ -5,8 +5,8 @@ @pytest.mark.asyncio -async def test_signal_send_and_data_received(django_asgi_app): - response, payload = await get_json(django_asgi_app, "/signals/send/") +async def test_signal_send_and_data_received(django_app): + response, payload = await get_json(django_app, "/signals/send/") assert response.status == 200 assert payload["sent"] is True @@ -15,8 +15,8 @@ async def test_signal_send_and_data_received(django_asgi_app): @pytest.mark.asyncio -async def test_signal_robust(django_asgi_app): - response, payload = await get_json(django_asgi_app, "/signals/robust/") +async def test_signal_robust(django_app): + response, payload = await get_json(django_app, "/signals/robust/") assert response.status == 200 assert payload["sent"] is True @@ -24,9 +24,9 @@ async def test_signal_robust(django_asgi_app): @pytest.mark.asyncio -async def test_signal_multiple_calls(django_asgi_app): - first_response, first_payload = await get_json(django_asgi_app, "/signals/send/") - second_response, second_payload = await get_json(django_asgi_app, "/signals/send/") +async def test_signal_multiple_calls(django_app): + first_response, first_payload = await get_json(django_app, "/signals/send/") + second_response, second_payload = await get_json(django_app, "/signals/send/") assert first_response.status == 200 assert second_response.status == 200 diff --git a/packages/runtime-sdk/tests/web-frameworks-test/django/src/test_streaming.py b/packages/runtime-sdk/tests/web-frameworks-test/django/src/test_streaming.py new file mode 100644 index 00000000..ac7de495 --- /dev/null +++ b/packages/runtime-sdk/tests/web-frameworks-test/django/src/test_streaming.py @@ -0,0 +1,28 @@ +import pytest +from _client import fetch, is_wsgi + + +def _streaming_paths(app): + if is_wsgi(app): + return ("/stream/sync-gen/",) + return ("/stream/async-gen/", "/stream/sync-gen/") + + +@pytest.mark.asyncio +async def test_streaming_generators(django_app): + for path in _streaming_paths(django_app): + response = await fetch(django_app, path) + body = await response.text() + + assert response.status == 200 + for index in range(5): + assert f"chunk-{index}" in body + + +@pytest.mark.asyncio +async def test_streaming_content_type(django_app): + response = await fetch(django_app, _streaming_paths(django_app)[0]) + + assert response.status == 200 + assert response.headers.get("Content-Type") is not None + assert response.headers.get("Content-Type").startswith("text/plain") diff --git a/packages/runtime-sdk/tests/web-frameworks-test/django-async/src/test_templates.py b/packages/runtime-sdk/tests/web-frameworks-test/django/src/test_templates.py similarity index 63% rename from packages/runtime-sdk/tests/web-frameworks-test/django-async/src/test_templates.py rename to packages/runtime-sdk/tests/web-frameworks-test/django/src/test_templates.py index b8cd122b..e0b5a39c 100644 --- a/packages/runtime-sdk/tests/web-frameworks-test/django-async/src/test_templates.py +++ b/packages/runtime-sdk/tests/web-frameworks-test/django/src/test_templates.py @@ -3,16 +3,16 @@ @pytest.mark.asyncio -async def test_template_hello(django_asgi_app): - response = await fetch(django_asgi_app, "/template/hello/") +async def test_template_hello(django_app): + response = await fetch(django_app, "/template/hello/") assert response.status == 200 assert "Hello, World!" in await response.text() @pytest.mark.asyncio -async def test_template_inheritance(django_asgi_app): - response = await fetch(django_asgi_app, "/template/inheritance/") +async def test_template_inheritance(django_app): + response = await fetch(django_app, "/template/inheritance/") assert response.status == 200 body = await response.text() @@ -20,8 +20,8 @@ async def test_template_inheritance(django_asgi_app): @pytest.mark.asyncio -async def test_template_context_variable(django_asgi_app): - response = await fetch(django_asgi_app, "/template/context/") +async def test_template_context_variable(django_app): + response = await fetch(django_app, "/template/context/") assert response.status == 200 body = await response.text() diff --git a/packages/runtime-sdk/tests/web-frameworks-test/django-async/src/test_url_routing.py b/packages/runtime-sdk/tests/web-frameworks-test/django/src/test_url_routing.py similarity index 52% rename from packages/runtime-sdk/tests/web-frameworks-test/django-async/src/test_url_routing.py rename to packages/runtime-sdk/tests/web-frameworks-test/django/src/test_url_routing.py index de292d1b..dfc7991d 100644 --- a/packages/runtime-sdk/tests/web-frameworks-test/django-async/src/test_url_routing.py +++ b/packages/runtime-sdk/tests/web-frameworks-test/django/src/test_url_routing.py @@ -5,65 +5,65 @@ @pytest.mark.asyncio -async def test_path_int_converter(django_asgi_app): +async def test_path_int_converter(django_app): for path, payload in ( ("/items/42/", {"id": 42, "type": "int"}), ("/users/alice/", {"name": "alice"}), ("/posts/my-first-post/", {"slug": "my-first-post"}), ): - response, data = await get_json(django_asgi_app, path) + response, data = await get_json(django_app, path) assert response.status == 200 assert data == payload uid = "550e8400-e29b-41d4-a716-446655440000" - response, payload = await get_json(django_asgi_app, f"/uuids/{uid}/") + response, payload = await get_json(django_app, f"/uuids/{uid}/") assert response.status == 200 assert uid in json.dumps(payload) @pytest.mark.asyncio -async def test_re_path(django_asgi_app): - response, payload = await get_json(django_asgi_app, "/archive/2024/") +async def test_re_path(django_app): + response, payload = await get_json(django_app, "/archive/2024/") assert response.status == 200 assert payload == {"year": "2024"} @pytest.mark.asyncio -async def test_include_namespace(django_asgi_app): - response, payload = await get_json(django_asgi_app, "/api/v1/info/") +async def test_include_namespace(django_app): + response, payload = await get_json(django_app, "/api/v1/info/") assert response.status == 200 assert payload == {"namespace": "api-v1"} @pytest.mark.asyncio -async def test_reverse_url(django_asgi_app): - response = await fetch(django_asgi_app, "/reverse-test/") +async def test_reverse_url(django_app): + response = await fetch(django_app, "/reverse-test/") assert response.status == 200 assert "/hello/" in await response.text() @pytest.mark.asyncio -async def test_unmatched_url_404(django_asgi_app): - response = await fetch(django_asgi_app, "/nonexistent/") +async def test_unmatched_url_404(django_app): + response = await fetch(django_app, "/nonexistent/") assert response.status == 404 @pytest.mark.asyncio -async def test_query_string_preserved(django_asgi_app): - response, payload = await get_json(django_asgi_app, "/echo-query/?foo=bar&baz=qux") +async def test_query_string_preserved(django_app): + response, payload = await get_json(django_app, "/echo-query/?foo=bar&baz=qux") assert response.status == 200 assert payload == {"foo": "bar", "baz": "qux"} @pytest.mark.asyncio -async def test_path_basic(django_asgi_app): - response = await fetch(django_asgi_app, "/hello/") +async def test_path_basic(django_app): + response = await fetch(django_app, "/hello/") assert response.status == 200 diff --git a/packages/runtime-sdk/tests/web-frameworks-test/django-async/src/test_views_basic.py b/packages/runtime-sdk/tests/web-frameworks-test/django/src/test_views_basic.py similarity index 55% rename from packages/runtime-sdk/tests/web-frameworks-test/django-async/src/test_views_basic.py rename to packages/runtime-sdk/tests/web-frameworks-test/django/src/test_views_basic.py index 071cde80..b5a982c4 100644 --- a/packages/runtime-sdk/tests/web-frameworks-test/django-async/src/test_views_basic.py +++ b/packages/runtime-sdk/tests/web-frameworks-test/django/src/test_views_basic.py @@ -5,64 +5,64 @@ @pytest.mark.asyncio -async def test_simple_async_view(django_asgi_app): - response = await fetch(django_asgi_app, "/hello/") +async def test_simple_async_view(django_app): + response = await fetch(django_app, "/hello/") assert response.status == 200 assert await response.text() == "hello" @pytest.mark.asyncio -async def test_status_codes(django_asgi_app): +async def test_status_codes(django_app): for path, status in ( ("/status/201/", 201), ("/status/204/", 204), ("/status/404/", 404), ): - response = await fetch(django_asgi_app, path) + response = await fetch(django_app, path) assert response.status == status @pytest.mark.asyncio -async def test_echo_method(django_asgi_app): +async def test_echo_method(django_app): for method in ["GET", "POST", "PUT", "DELETE", "PATCH"]: if method == "POST": - response = await post_json(django_asgi_app, "/echo-method/", {"ok": True}) + response = await post_json(django_app, "/echo-method/", {"ok": True}) else: - response = await fetch(django_asgi_app, "/echo-method/", method=method) + response = await fetch(django_app, "/echo-method/", method=method) assert response.status == 200 assert json.loads(await response.text()) == {"method": method} @pytest.mark.asyncio -async def test_async_cbv_get(django_asgi_app): - response = await fetch(django_asgi_app, "/cbv/") +async def test_async_cbv_get(django_app): + response = await fetch(django_app, "/cbv/") assert response.status == 200 assert json.loads(await response.text()) == {"method": "GET"} @pytest.mark.asyncio -async def test_async_cbv_post(django_asgi_app): - response = await post_json(django_asgi_app, "/cbv/", {"hello": "world"}) +async def test_async_cbv_post(django_app): + response = await post_json(django_app, "/cbv/", {"hello": "world"}) assert response.status == 200 assert json.loads(await response.text()) == {"method": "POST"} @pytest.mark.asyncio -async def test_head_request(django_asgi_app): - response = await fetch(django_asgi_app, "/hello/", method="HEAD") +async def test_head_request(django_app): + response = await fetch(django_app, "/hello/", method="HEAD") assert response.status == 200 assert response.headers.has("Content-Type") @pytest.mark.asyncio -async def test_options_returns_allowed(django_asgi_app): - response = await fetch(django_asgi_app, "/cbv/", method="OPTIONS") +async def test_options_returns_allowed(django_app): + response = await fetch(django_app, "/cbv/", method="OPTIONS") assert response.status == 200 allow = response.headers.get("Allow") diff --git a/packages/runtime-sdk/tests/web-frameworks-test/django-async/src/worker.py b/packages/runtime-sdk/tests/web-frameworks-test/django/src/worker.py similarity index 78% rename from packages/runtime-sdk/tests/web-frameworks-test/django-async/src/worker.py rename to packages/runtime-sdk/tests/web-frameworks-test/django/src/worker.py index c42f9a7e..946e9367 100644 --- a/packages/runtime-sdk/tests/web-frameworks-test/django-async/src/worker.py +++ b/packages/runtime-sdk/tests/web-frameworks-test/django/src/worker.py @@ -40,7 +40,7 @@ async def _noop(*args): "django.middleware.common.CommonMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", - "django_app.middleware.CustomAsyncMiddleware", + "django_app.middleware.CustomMiddleware", ], TEMPLATES=[ { @@ -91,8 +91,26 @@ async def _noop(*args): django.setup() from django.core.handlers.asgi import ASGIHandler # noqa: E402 +from django.core.handlers.wsgi import WSGIHandler # noqa: E402 + + +class SyncWSGIHandler(WSGIHandler): + """WSGI handler serving the sync mirror of the URLconf. + + Django adapts async views under WSGI with ``async_to_sync``, which raises + when a loop is already running (always true in workerd). The WSGI side + therefore resolves against ``urls_sync``, whose views are natively sync. + """ + + def get_response(self, request): + request.urlconf = "django_app.urls_sync" + return super().get_response(request) + django_asgi_app = ASGIHandler() +django_wsgi_app = SyncWSGIHandler() + +APPS = {"asgi": django_asgi_app, "wsgi": django_wsgi_app} class ResultCollector: @@ -156,32 +174,51 @@ def env(self): class DjangoAppPlugin: + def __init__(self, mode): + self._mode = mode + + def pytest_configure(self, config): + config.addinivalue_line( + "markers", "asgi_only: run only against the ASGI app, skip otherwise" + ) + + def pytest_runtest_setup(self, item): + if self._mode != "asgi" and item.get_closest_marker("asgi_only"): + pytest.skip(f"async-only Django feature, not applicable to {self._mode}") + @pytest.fixture - def django_asgi_app(self): - return django_asgi_app + def django_app(self): + return APPS[self._mode] class Default(WorkerEntrypoint): async def fetch(self, request): - from urllib.parse import urlparse + from urllib.parse import parse_qs, urlparse - path = urlparse(request.url).path + parsed = urlparse(request.url) + path = parsed.path if path.startswith("/run-tests/"): suite_name = path[len("/run-tests/") :] - return self._run_suite(suite_name) + mode = parse_qs(parsed.query).get("mode", ["asgi"])[0] + return self._run_suite(suite_name, mode) if path == "/health": return Response.json({"ok": True}) return await asgi.fetch(django_asgi_app, request, self.env, self.ctx) - def _run_suite(self, suite_name): + def _run_suite(self, suite_name, mode): module = f"test_{suite_name}" if importlib.util.find_spec(module) is None: return Response.json( {"error": f"Unknown suite '{suite_name}' (no module '{module}')"}, status=404, ) + if mode not in APPS: + return Response.json( + {"error": f"Unknown mode '{mode}' (expected one of {sorted(APPS)})"}, + status=400, + ) collector = ResultCollector() # pytest-asyncio drives each test through asyncio.Runner, which calls @@ -198,7 +235,7 @@ def _run_suite(self, suite_name): plugins=[ collector, EnvPlugin(self.env), - DjangoAppPlugin(), + DjangoAppPlugin(mode), ], ) finally: diff --git a/packages/runtime-sdk/tests/web-frameworks-test/django-async/wrangler.jsonc b/packages/runtime-sdk/tests/web-frameworks-test/django/wrangler.jsonc similarity index 78% rename from packages/runtime-sdk/tests/web-frameworks-test/django-async/wrangler.jsonc rename to packages/runtime-sdk/tests/web-frameworks-test/django/wrangler.jsonc index 6cb52de7..9bf6fb31 100644 --- a/packages/runtime-sdk/tests/web-frameworks-test/django-async/wrangler.jsonc +++ b/packages/runtime-sdk/tests/web-frameworks-test/django/wrangler.jsonc @@ -1,5 +1,5 @@ { - "name": "django-async-test", + "name": "django-test", "main": "src/worker.py", "compatibility_date": "%COMPAT_DATE", "compatibility_flags": ["python_workers"]