Skip to content

Commit 6dbb635

Browse files
committed
test(django): add middleware tests for Django 5 ASGI validation
Add comprehensive test suite for PostHog Django middleware in async context: - Test async user access (unauthenticated) - Test async authenticated user access (triggers SynchronousOnlyOperation in v6.7.11) - Test sync user access - Test async exception capture - Test sync exception capture Tests run directly against ASGI application using httpx AsyncClient without needing a server. Uses pytest-asyncio for async test support. The authenticated user test demonstrates the bug fixed in this PR: v6.7.11 raises SynchronousOnlyOperation when accessing request.user in async middleware with authenticated users. The fix uses await request.auser() instead. Add test dependencies: pytest, pytest-asyncio, httpx
1 parent 4799b7b commit 6dbb635

File tree

4 files changed

+298
-0
lines changed

4 files changed

+298
-0
lines changed

test_project_django5/.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
db.sqlite3
2+
*.pyc
3+
__pycache__/
4+
.pytest_cache/

test_project_django5/pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ dependencies = [
66
"django>=5.0",
77
"uvicorn[standard]",
88
"posthog",
9+
"pytest>=8.0",
10+
"pytest-asyncio>=0.23",
11+
"httpx>=0.27",
912
]
1013

1114
[tool.uv.sources]
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
"""
2+
Tests for PostHog Django middleware in async context.
3+
4+
These tests verify that the middleware correctly handles:
5+
1. Async user access (request.auser() in Django 5)
6+
2. Exception capture in both sync and async views
7+
3. No SynchronousOnlyOperation errors in async context
8+
9+
Tests run directly against the ASGI application without needing a server.
10+
"""
11+
import os
12+
import django
13+
14+
# Setup Django before importing anything else
15+
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "testdjango.settings")
16+
django.setup()
17+
18+
import pytest
19+
from httpx import AsyncClient, ASGITransport
20+
from django.core.asgi import get_asgi_application
21+
22+
23+
@pytest.mark.asyncio
24+
async def test_async_user_access():
25+
"""
26+
Test that middleware can access request.user in async context.
27+
28+
In Django 5, this requires using await request.auser() instead of request.user
29+
to avoid SynchronousOnlyOperation error.
30+
31+
Without authentication, request.user is AnonymousUser which doesn't
32+
trigger the lazy loading bug. This test verifies the middleware works
33+
in the common case.
34+
"""
35+
app = get_asgi_application()
36+
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://testserver") as ac:
37+
response = await ac.get("/test/async-user")
38+
39+
assert response.status_code == 200
40+
data = response.json()
41+
assert data["status"] == "success"
42+
assert "django_version" in data
43+
print(f"✓ Async user access test passed: {data['message']}")
44+
45+
46+
@pytest.mark.asyncio
47+
async def test_async_authenticated_user_access():
48+
"""
49+
Test that middleware can access an authenticated user in async context.
50+
51+
This is the critical test that triggers the SynchronousOnlyOperation bug
52+
in v6.7.11. When AuthenticationMiddleware sets request.user to a
53+
SimpleLazyObject wrapping a database query, accessing user.pk or user.email
54+
in async context causes the error.
55+
56+
In v6.7.11, extract_request_user() does getattr(user, "is_authenticated", False)
57+
which triggers the lazy object evaluation synchronously.
58+
59+
The fix uses await request.auser() instead to avoid this.
60+
"""
61+
from django.contrib.auth import get_user_model
62+
from django.test import Client
63+
from asgiref.sync import sync_to_async
64+
from django.test import override_settings
65+
66+
# Create a test user (must use sync_to_async since we're in async test)
67+
User = get_user_model()
68+
69+
@sync_to_async
70+
def create_or_get_user():
71+
user, created = User.objects.get_or_create(
72+
username='testuser',
73+
defaults={
74+
'email': '[email protected]',
75+
}
76+
)
77+
if created:
78+
user.set_password('testpass123')
79+
user.save()
80+
return user
81+
82+
user = await create_or_get_user()
83+
84+
# Create a session with authenticated user (sync operation)
85+
@sync_to_async
86+
def create_session():
87+
client = Client()
88+
client.force_login(user)
89+
return client.cookies.get('sessionid')
90+
91+
session_cookie = await create_session()
92+
93+
if not session_cookie:
94+
print("⚠ Warning: Could not create authenticated session, skipping auth test")
95+
return
96+
97+
# Make request with session cookie - this should trigger the bug in v6.7.11
98+
# Disable exception capture to see the SynchronousOnlyOperation clearly
99+
with override_settings(POSTHOG_MW_CAPTURE_EXCEPTIONS=False):
100+
app = get_asgi_application()
101+
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://testserver") as ac:
102+
response = await ac.get(
103+
"/test/async-user",
104+
cookies={"sessionid": session_cookie.value}
105+
)
106+
107+
assert response.status_code == 200
108+
data = response.json()
109+
assert data["status"] == "success"
110+
assert data["user_authenticated"] == True
111+
print(f"✓ Async authenticated user access test passed: {data['message']}")
112+
113+
114+
@pytest.mark.asyncio
115+
async def test_sync_user_access():
116+
"""
117+
Test that middleware works with sync views.
118+
119+
This should always work regardless of middleware version.
120+
"""
121+
app = get_asgi_application()
122+
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://testserver") as ac:
123+
response = await ac.get("/test/sync-user")
124+
125+
assert response.status_code == 200
126+
data = response.json()
127+
assert data["status"] == "success"
128+
print(f"✓ Sync user access test passed: {data['message']}")
129+
130+
131+
@pytest.mark.asyncio
132+
async def test_async_exception_capture():
133+
"""
134+
Test that middleware captures exceptions from async views.
135+
136+
The middleware should capture the exception and send it to PostHog.
137+
"""
138+
app = get_asgi_application()
139+
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://testserver") as ac:
140+
response = await ac.get("/test/async-exception")
141+
142+
# Django returns 500 for unhandled exceptions
143+
assert response.status_code == 500
144+
print("✓ Async exception capture test passed (exception raised as expected)")
145+
146+
147+
@pytest.mark.asyncio
148+
async def test_sync_exception_capture():
149+
"""
150+
Test that middleware captures exceptions from sync views.
151+
152+
The middleware should capture the exception and send it to PostHog.
153+
"""
154+
app = get_asgi_application()
155+
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://testserver") as ac:
156+
response = await ac.get("/test/sync-exception")
157+
158+
# Django returns 500 for unhandled exceptions
159+
assert response.status_code == 500
160+
print("✓ Sync exception capture test passed (exception raised as expected)")
161+
162+
163+
if __name__ == "__main__":
164+
"""Run tests directly with asyncio for quick testing."""
165+
import asyncio
166+
167+
async def run_all_tests():
168+
print("\nRunning PostHog Django middleware tests...\n")
169+
170+
try:
171+
await test_async_user_access()
172+
except Exception as e:
173+
print(f"✗ Async user access test failed: {e}")
174+
175+
try:
176+
await test_sync_user_access()
177+
except Exception as e:
178+
print(f"✗ Sync user access test failed: {e}")
179+
180+
try:
181+
await test_async_exception_capture()
182+
except Exception as e:
183+
print(f"✗ Async exception capture test failed: {e}")
184+
185+
try:
186+
await test_sync_exception_capture()
187+
except Exception as e:
188+
print(f"✗ Sync exception capture test failed: {e}")
189+
190+
print("\nAll tests completed!\n")
191+
192+
asyncio.run(run_all_tests())

test_project_django5/uv.lock

Lines changed: 99 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)