diff --git a/packages/django-cf/tests/in_worker/worker/src/_asgi_client.py b/packages/django-cf/tests/in_worker/worker/src/_asgi_client.py new file mode 100644 index 0000000..6d29d92 --- /dev/null +++ b/packages/django-cf/tests/in_worker/worker/src/_asgi_client.py @@ -0,0 +1,36 @@ +# pyright: reportMissingImports=false + +import json as _json + +import asgi as _asgi +from workers import Request + +BASE_URL = "http://testserver" + + +def _with_content_length(headers, body): + hdrs = dict(headers or {}) + if body is not None and not any(key.lower() == "content-length" for key in hdrs): + length = len(body.encode() if isinstance(body, str) else body) + hdrs["Content-Length"] = str(length) + return hdrs + + +async def fetch(app, path, *, method="GET", headers=None, body=None, env=None): + request = Request( + f"{BASE_URL}{path}", + method=method, + headers=_with_content_length(headers, body), + body=body, + ) + return await _asgi.fetch(app, request, {} if env is None else env) + + +async def read_json(response): + text = await response.text() + return _json.loads(text) if text else None + + +async def get_json(app, path, **kwargs): + response = await fetch(app, path, **kwargs) + return response, await read_json(response) diff --git a/packages/django-cf/tests/in_worker/worker/src/test_asgi_d1.py b/packages/django-cf/tests/in_worker/worker/src/test_asgi_d1.py new file mode 100644 index 0000000..d42fe14 --- /dev/null +++ b/packages/django-cf/tests/in_worker/worker/src/test_asgi_d1.py @@ -0,0 +1,156 @@ +"""ASGI D1 tests executed inside workerd.""" + +# pyright: reportMissingImports=false + +import pytest +from _asgi_client import get_json +from django.core.handlers.asgi import ASGIHandler +from django.db import connections, models +from django.http import JsonResponse +from django.test import override_settings +from django.urls import path + +D1_TABLE = "_django_cf_asgi_d1_records" + + +class AsgiD1Record(models.Model): + value = models.CharField(max_length=64) + + class Meta: + app_label = "django_cf_in_worker" + db_table = D1_TABLE + managed = False + + +async def create_and_read_view(request): + del request + created = await AsgiD1Record.objects.using("d1").acreate(value="d1-ok") + loaded = await AsgiD1Record.objects.using("d1").aget(pk=created.pk) + return JsonResponse({"id": loaded.pk, "value": loaded.value}) + + +async def count_view(request): + del request + await AsgiD1Record.objects.using("d1").acreate(value="alpha") + await AsgiD1Record.objects.using("d1").acreate(value="beta") + count = await AsgiD1Record.objects.using("d1").acount() # codespell:ignore acount + return JsonResponse({"count": count}) + + +async def exists_view(request): + del request + await AsgiD1Record.objects.using("d1").acreate(value="present") + matching = await AsgiD1Record.objects.using("d1").filter(value="present").aexists() + missing = await AsgiD1Record.objects.using("d1").filter(value="missing").aexists() + return JsonResponse({"matching": matching, "missing": missing}) + + +async def update_view(request): + del request + created = await AsgiD1Record.objects.using("d1").acreate(value="before-update") + updated = ( + await AsgiD1Record.objects.using("d1") + .filter(pk=created.pk) + .aupdate(value="after-update") + ) + loaded = await AsgiD1Record.objects.using("d1").aget(pk=created.pk) + return JsonResponse({"id": loaded.pk, "updated": updated, "value": loaded.value}) + + +async def iterate_view(request): + del request + for value in ["charlie", "alpha", "bravo"]: + await AsgiD1Record.objects.using("d1").acreate(value=value) + + values = [] + queryset = AsgiD1Record.objects.using("d1").order_by("value") + async for record in queryset: + values.append(record.value) + + return JsonResponse({"values": values}) + + +urlpatterns = [ + path("asgi/d1/create-read/", create_and_read_view), + path("asgi/d1/count/", count_view), + path("asgi/d1/exists/", exists_view), + path("asgi/d1/update/", update_view), + path("asgi/d1/iterate/", iterate_view), +] + + +async def _fetch_d1_response(path_name): + with override_settings(ROOT_URLCONF=__name__): + app = ASGIHandler() + return await get_json(app, path_name) + + +def _d1_connection(): + return connections["d1"] + + +def _drop_d1_table(): + _d1_connection().run_query(f"DROP TABLE IF EXISTS {D1_TABLE}") + + +def _create_d1_table(): + _drop_d1_table() + _d1_connection().run_query( + f"CREATE TABLE {D1_TABLE} (id INTEGER PRIMARY KEY AUTOINCREMENT, value TEXT NOT NULL)" + ) + + +async def _run_d1_request(path_name): + _create_d1_table() + try: + return await _fetch_d1_response(path_name) + finally: + _drop_d1_table() + + +@pytest.mark.asyncio +async def test_asgi_d1_orm_create_and_read(): + response, payload = await _run_d1_request("/asgi/d1/create-read/") + + assert response.status == 200 + assert payload is not None + assert payload["value"] == "d1-ok" + assert isinstance(payload["id"], int) + assert payload["id"] > 0 + + +@pytest.mark.asyncio +async def test_asgi_d1_orm_count_returns_row_total(): + response, payload = await _run_d1_request("/asgi/d1/count/") + + assert response.status == 200 + assert payload == {"count": 2} + + +@pytest.mark.asyncio +async def test_asgi_d1_orm_exists_reports_matching_and_missing_filters(): + response, payload = await _run_d1_request("/asgi/d1/exists/") + + assert response.status == 200 + assert payload == {"matching": True, "missing": False} + + +@pytest.mark.asyncio +async def test_asgi_d1_orm_update_returns_affected_rows_and_persists_value(): + response, payload = await _run_d1_request("/asgi/d1/update/") + + assert response.status == 200 + assert payload is not None + assert payload["updated"] == 1 + assert payload["value"] == "after-update" + assert isinstance(payload["id"], int) + assert payload["id"] > 0 + + +@pytest.mark.asyncio +async def test_asgi_d1_orm_async_iteration_returns_all_rows(): + response, payload = await _run_d1_request("/asgi/d1/iterate/") + + assert response.status == 200 + assert payload is not None + assert sorted(payload["values"]) == ["alpha", "bravo", "charlie"] diff --git a/packages/django-cf/tests/in_worker/worker/src/test_asgi_r2.py b/packages/django-cf/tests/in_worker/worker/src/test_asgi_r2.py new file mode 100644 index 0000000..1ccf985 --- /dev/null +++ b/packages/django-cf/tests/in_worker/worker/src/test_asgi_r2.py @@ -0,0 +1,52 @@ +"""ASGI R2 tests executed inside workerd.""" + +# pyright: reportMissingImports=false + +from uuid import uuid4 + +import pytest +from _asgi_client import fetch +from django.core.files.base import ContentFile +from django.core.handlers.asgi import ASGIHandler +from django.http import HttpResponse +from django.test import override_settings +from django.urls import path + +from django_cf.storage import R2Storage + + +async def r2_view(request): + del request + storage = R2Storage(binding="BUCKET", location=f"asgi-r2-{uuid4().hex}") + content = b"asgi-r2-content" + saved_name = None + + try: + saved_name = storage.save("payload.bin", ContentFile(content)) + r2_file = storage.open(saved_name, "rb") + try: + loaded = r2_file.read() + finally: + r2_file.close() + finally: + if saved_name is not None: + storage.delete(saved_name) + + return HttpResponse(loaded, content_type="application/octet-stream") + + +urlpatterns = [path("asgi/r2/", r2_view)] + + +async def _fetch_r2_response(path_name): + with override_settings(ROOT_URLCONF=__name__): + app = ASGIHandler() + return await fetch(app, path_name) + + +@pytest.mark.asyncio +async def test_asgi_r2_save_and_read(): + response = await _fetch_r2_response("/asgi/r2/") + + assert response.status == 200 + assert await response.text() == "asgi-r2-content"