django-bolt 0.1.0__cp310-abi3-win_amd64.whl → 0.1.2__cp310-abi3-win_amd64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of django-bolt might be problematic. Click here for more details.
- django_bolt/__init__.py +2 -2
- django_bolt/_core.pyd +0 -0
- django_bolt/_json.py +169 -0
- django_bolt/admin/static_routes.py +15 -21
- django_bolt/api.py +181 -61
- django_bolt/auth/__init__.py +2 -2
- django_bolt/decorators.py +15 -3
- django_bolt/dependencies.py +30 -24
- django_bolt/error_handlers.py +2 -1
- django_bolt/openapi/plugins.py +3 -2
- django_bolt/openapi/schema_generator.py +65 -20
- django_bolt/pagination.py +2 -1
- django_bolt/responses.py +3 -2
- django_bolt/serialization.py +5 -4
- {django_bolt-0.1.0.dist-info → django_bolt-0.1.2.dist-info}/METADATA +181 -201
- {django_bolt-0.1.0.dist-info → django_bolt-0.1.2.dist-info}/RECORD +18 -55
- django_bolt/auth/README.md +0 -464
- django_bolt/auth/REVOCATION_EXAMPLE.md +0 -391
- django_bolt/tests/__init__.py +0 -0
- django_bolt/tests/admin_tests/__init__.py +0 -1
- django_bolt/tests/admin_tests/conftest.py +0 -6
- django_bolt/tests/admin_tests/test_admin_with_django.py +0 -278
- django_bolt/tests/admin_tests/urls.py +0 -9
- django_bolt/tests/cbv/__init__.py +0 -0
- django_bolt/tests/cbv/test_class_views.py +0 -570
- django_bolt/tests/cbv/test_class_views_django_orm.py +0 -703
- django_bolt/tests/cbv/test_class_views_features.py +0 -1173
- django_bolt/tests/cbv/test_class_views_with_client.py +0 -622
- django_bolt/tests/conftest.py +0 -165
- django_bolt/tests/test_action_decorator.py +0 -399
- django_bolt/tests/test_auth_secret_key.py +0 -83
- django_bolt/tests/test_decorator_syntax.py +0 -159
- django_bolt/tests/test_error_handling.py +0 -481
- django_bolt/tests/test_file_response.py +0 -192
- django_bolt/tests/test_global_cors.py +0 -172
- django_bolt/tests/test_guards_auth.py +0 -441
- django_bolt/tests/test_guards_integration.py +0 -303
- django_bolt/tests/test_health.py +0 -283
- django_bolt/tests/test_integration_validation.py +0 -400
- django_bolt/tests/test_json_validation.py +0 -536
- django_bolt/tests/test_jwt_auth.py +0 -327
- django_bolt/tests/test_jwt_token.py +0 -458
- django_bolt/tests/test_logging.py +0 -837
- django_bolt/tests/test_logging_merge.py +0 -419
- django_bolt/tests/test_middleware.py +0 -492
- django_bolt/tests/test_middleware_server.py +0 -230
- django_bolt/tests/test_model_viewset.py +0 -323
- django_bolt/tests/test_models.py +0 -24
- django_bolt/tests/test_pagination.py +0 -1258
- django_bolt/tests/test_parameter_validation.py +0 -178
- django_bolt/tests/test_syntax.py +0 -626
- django_bolt/tests/test_testing_utilities.py +0 -163
- django_bolt/tests/test_testing_utilities_simple.py +0 -123
- django_bolt/tests/test_viewset_unified.py +0 -346
- {django_bolt-0.1.0.dist-info → django_bolt-0.1.2.dist-info}/WHEEL +0 -0
- {django_bolt-0.1.0.dist-info → django_bolt-0.1.2.dist-info}/entry_points.txt +0 -0
|
@@ -1,230 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Integration test for middleware with TestClient
|
|
3
|
-
"""
|
|
4
|
-
import jwt
|
|
5
|
-
import time
|
|
6
|
-
import pytest
|
|
7
|
-
from django_bolt import BoltAPI
|
|
8
|
-
from django_bolt.middleware import rate_limit, cors
|
|
9
|
-
from django_bolt.auth import JWTAuthentication, APIKeyAuthentication
|
|
10
|
-
from django_bolt.auth import IsAuthenticated
|
|
11
|
-
from django_bolt.testing import TestClient
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
@pytest.fixture(scope="module")
|
|
15
|
-
def api():
|
|
16
|
-
"""Create test API with various middleware configurations"""
|
|
17
|
-
# Setup minimal Django for testing
|
|
18
|
-
import django
|
|
19
|
-
from django.conf import settings
|
|
20
|
-
|
|
21
|
-
if not settings.configured:
|
|
22
|
-
settings.configure(
|
|
23
|
-
DEBUG=True,
|
|
24
|
-
SECRET_KEY='test-secret-key-for-middleware',
|
|
25
|
-
INSTALLED_APPS=[
|
|
26
|
-
'django.contrib.contenttypes',
|
|
27
|
-
'django.contrib.auth',
|
|
28
|
-
'django_bolt',
|
|
29
|
-
],
|
|
30
|
-
DATABASES={
|
|
31
|
-
'default': {
|
|
32
|
-
'ENGINE': 'django.db.backends.sqlite3',
|
|
33
|
-
'NAME': ':memory:',
|
|
34
|
-
}
|
|
35
|
-
},
|
|
36
|
-
USE_TZ=True,
|
|
37
|
-
)
|
|
38
|
-
django.setup()
|
|
39
|
-
|
|
40
|
-
api = BoltAPI(
|
|
41
|
-
middleware_config={
|
|
42
|
-
'cors': {
|
|
43
|
-
'origins': ['http://localhost:3000'],
|
|
44
|
-
'credentials': True
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
)
|
|
48
|
-
|
|
49
|
-
@api.get("/")
|
|
50
|
-
async def root():
|
|
51
|
-
return {"message": "Hello, middleware!"}
|
|
52
|
-
|
|
53
|
-
@api.get("/rate-limited")
|
|
54
|
-
@rate_limit(rps=5, burst=10)
|
|
55
|
-
async def rate_limited_endpoint():
|
|
56
|
-
return {"message": "This endpoint is rate limited", "timestamp": time.time()}
|
|
57
|
-
|
|
58
|
-
@api.get("/cors-test")
|
|
59
|
-
@cors(origins=["http://localhost:3000", "http://example.com"], credentials=True)
|
|
60
|
-
async def cors_endpoint():
|
|
61
|
-
return {"cors": "enabled"}
|
|
62
|
-
|
|
63
|
-
@api.get(
|
|
64
|
-
"/protected-jwt",
|
|
65
|
-
auth=[JWTAuthentication(secret="test-secret")],
|
|
66
|
-
guards=[IsAuthenticated()]
|
|
67
|
-
)
|
|
68
|
-
async def jwt_protected():
|
|
69
|
-
return {"message": "JWT protected content"}
|
|
70
|
-
|
|
71
|
-
@api.get(
|
|
72
|
-
"/protected-api-key",
|
|
73
|
-
auth=[APIKeyAuthentication(
|
|
74
|
-
api_keys={"test-key-123", "test-key-456"},
|
|
75
|
-
header="authorization"
|
|
76
|
-
)],
|
|
77
|
-
guards=[IsAuthenticated()]
|
|
78
|
-
)
|
|
79
|
-
async def api_key_protected():
|
|
80
|
-
return {"message": "API key protected content"}
|
|
81
|
-
|
|
82
|
-
@api.get(
|
|
83
|
-
"/context-test",
|
|
84
|
-
auth=[APIKeyAuthentication(
|
|
85
|
-
api_keys={"test-key"},
|
|
86
|
-
header="authorization"
|
|
87
|
-
)],
|
|
88
|
-
guards=[IsAuthenticated()]
|
|
89
|
-
)
|
|
90
|
-
async def context_endpoint(request: dict):
|
|
91
|
-
"""Test that middleware context is available"""
|
|
92
|
-
context = request.get("context", None)
|
|
93
|
-
return {
|
|
94
|
-
"has_context": context is not None,
|
|
95
|
-
"context_keys": list(context.keys()) if context and hasattr(context, 'keys') else []
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
return api
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
@pytest.fixture(scope="module")
|
|
102
|
-
def client(api):
|
|
103
|
-
"""Create TestClient for the API (fast mode - direct dispatch)"""
|
|
104
|
-
with TestClient(api) as client:
|
|
105
|
-
yield client
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
@pytest.fixture(scope="module")
|
|
109
|
-
def http_client(api):
|
|
110
|
-
"""Create TestClient with HTTP layer enabled (for middleware testing)"""
|
|
111
|
-
with TestClient(api, use_http_layer=True) as client:
|
|
112
|
-
yield client
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
def test_basic_endpoint(client):
|
|
116
|
-
"""Test basic endpoint"""
|
|
117
|
-
response = client.get("/")
|
|
118
|
-
assert response.status_code == 200
|
|
119
|
-
assert response.json() == {"message": "Hello, middleware!"}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
def test_compression_http_layer(http_client):
|
|
123
|
-
"""Test that compression middleware is applied in HTTP layer"""
|
|
124
|
-
# Request with Accept-Encoding header
|
|
125
|
-
response = http_client.get("/", headers={"Accept-Encoding": "gzip"})
|
|
126
|
-
assert response.status_code == 200
|
|
127
|
-
assert response.json() == {"message": "Hello, middleware!"}
|
|
128
|
-
# Note: httpx automatically decompresses, so we won't see content-encoding in response
|
|
129
|
-
# But the middleware is applied in Actix layer
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
def test_rate_limiting(http_client):
|
|
133
|
-
"""Test rate limiting"""
|
|
134
|
-
# The rate limit is 5 rps with burst of 10
|
|
135
|
-
# First 10 requests should succeed (burst)
|
|
136
|
-
for i in range(10):
|
|
137
|
-
response = http_client.get("/rate-limited")
|
|
138
|
-
assert response.status_code == 200, f"Request {i+1} failed"
|
|
139
|
-
|
|
140
|
-
# Next request should be rate limited
|
|
141
|
-
response = http_client.get("/rate-limited")
|
|
142
|
-
assert response.status_code == 429 # Too Many Requests
|
|
143
|
-
assert "retry-after" in response.headers
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
def test_cors_headers(http_client):
|
|
147
|
-
"""Test CORS headers"""
|
|
148
|
-
response = http_client.get("/cors-test", headers={"Origin": "http://localhost:3000"})
|
|
149
|
-
assert response.status_code == 200
|
|
150
|
-
assert response.json() == {"cors": "enabled"}
|
|
151
|
-
# Check CORS headers
|
|
152
|
-
assert "access-control-allow-origin" in response.headers
|
|
153
|
-
assert response.headers["access-control-allow-origin"] == "http://localhost:3000"
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
def test_cors_preflight(http_client):
|
|
157
|
-
"""Test CORS preflight (OPTIONS)"""
|
|
158
|
-
response = http_client.options(
|
|
159
|
-
"/cors-test",
|
|
160
|
-
headers={
|
|
161
|
-
"Origin": "http://localhost:3000",
|
|
162
|
-
"Access-Control-Request-Method": "GET",
|
|
163
|
-
"Access-Control-Request-Headers": "Content-Type"
|
|
164
|
-
}
|
|
165
|
-
)
|
|
166
|
-
# Preflight should return 204
|
|
167
|
-
assert response.status_code == 204
|
|
168
|
-
# Check preflight headers
|
|
169
|
-
assert "access-control-allow-origin" in response.headers
|
|
170
|
-
assert "access-control-allow-methods" in response.headers
|
|
171
|
-
assert "access-control-allow-headers" in response.headers
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
def test_jwt_auth_without_token(client):
|
|
175
|
-
"""Test JWT authentication without token"""
|
|
176
|
-
response = client.get("/protected-jwt")
|
|
177
|
-
assert response.status_code == 401
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
def test_jwt_auth_with_valid_token(client):
|
|
181
|
-
"""Test JWT authentication with valid token"""
|
|
182
|
-
token = jwt.encode(
|
|
183
|
-
{"sub": "user123", "exp": int(time.time()) + 3600},
|
|
184
|
-
"test-secret",
|
|
185
|
-
algorithm="HS256"
|
|
186
|
-
)
|
|
187
|
-
response = client.get("/protected-jwt", headers={"Authorization": f"Bearer {token}"})
|
|
188
|
-
assert response.status_code == 200
|
|
189
|
-
assert response.json() == {"message": "JWT protected content"}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
def test_jwt_auth_with_expired_token(client):
|
|
193
|
-
"""Test JWT authentication with expired token"""
|
|
194
|
-
expired_token = jwt.encode(
|
|
195
|
-
{"sub": "user123", "exp": int(time.time()) - 3600},
|
|
196
|
-
"test-secret",
|
|
197
|
-
algorithm="HS256"
|
|
198
|
-
)
|
|
199
|
-
response = client.get("/protected-jwt", headers={"Authorization": f"Bearer {expired_token}"})
|
|
200
|
-
assert response.status_code == 401
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
def test_api_key_auth_without_key(client):
|
|
204
|
-
"""Test API key authentication without key"""
|
|
205
|
-
response = client.get("/protected-api-key")
|
|
206
|
-
assert response.status_code == 401
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
def test_api_key_auth_with_valid_key(client):
|
|
210
|
-
"""Test API key authentication with valid key"""
|
|
211
|
-
response = client.get("/protected-api-key", headers={"Authorization": "Bearer test-key-123"})
|
|
212
|
-
assert response.status_code == 200
|
|
213
|
-
assert response.json() == {"message": "API key protected content"}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
def test_api_key_auth_with_invalid_key(client):
|
|
217
|
-
"""Test API key authentication with invalid key"""
|
|
218
|
-
response = client.get("/protected-api-key", headers={"Authorization": "Bearer invalid-key"})
|
|
219
|
-
assert response.status_code == 401
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
def test_context_availability(client):
|
|
223
|
-
"""Test middleware context availability"""
|
|
224
|
-
response = client.get("/context-test", headers={"Authorization": "Bearer test-key"})
|
|
225
|
-
assert response.status_code == 200
|
|
226
|
-
data = response.json()
|
|
227
|
-
# Context may or may not be available depending on implementation
|
|
228
|
-
# Just verify the endpoint works and returns expected structure
|
|
229
|
-
assert "has_context" in data
|
|
230
|
-
assert "context_keys" in data
|
|
@@ -1,323 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Tests for ModelViewSet and ReadOnlyModelViewSet (DRF-style usage).
|
|
3
|
-
|
|
4
|
-
This test suite verifies that ModelViewSet and ReadOnlyModelViewSet work similarly
|
|
5
|
-
to Django REST Framework's ModelViewSet, where you just set queryset and serializer_class.
|
|
6
|
-
"""
|
|
7
|
-
import pytest
|
|
8
|
-
import msgspec
|
|
9
|
-
from django_bolt import BoltAPI, ModelViewSet, ReadOnlyModelViewSet
|
|
10
|
-
from django_bolt.testing import TestClient
|
|
11
|
-
from .test_models import Article
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
# --- Schemas ---
|
|
15
|
-
|
|
16
|
-
class ArticleSchema(msgspec.Struct):
|
|
17
|
-
"""Full article schema."""
|
|
18
|
-
id: int
|
|
19
|
-
title: str
|
|
20
|
-
content: str
|
|
21
|
-
author: str
|
|
22
|
-
is_published: bool
|
|
23
|
-
|
|
24
|
-
@classmethod
|
|
25
|
-
def from_model(cls, obj):
|
|
26
|
-
return cls(
|
|
27
|
-
id=obj.id,
|
|
28
|
-
title=obj.title,
|
|
29
|
-
content=obj.content,
|
|
30
|
-
author=obj.author,
|
|
31
|
-
is_published=obj.is_published,
|
|
32
|
-
)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
class ArticleCreateSchema(msgspec.Struct):
|
|
36
|
-
"""Schema for creating/updating articles."""
|
|
37
|
-
title: str
|
|
38
|
-
content: str
|
|
39
|
-
author: str
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
# --- Tests ---
|
|
43
|
-
|
|
44
|
-
@pytest.mark.django_db(transaction=True)
|
|
45
|
-
def test_readonly_model_viewset(api):
|
|
46
|
-
"""Test ReadOnlyModelViewSet provides helpers for read operations."""
|
|
47
|
-
from asgiref.sync import async_to_sync
|
|
48
|
-
|
|
49
|
-
# Create test data
|
|
50
|
-
article1 = async_to_sync(Article.objects.acreate)(
|
|
51
|
-
title="Article 1",
|
|
52
|
-
content="Content 1",
|
|
53
|
-
author="Author 1",
|
|
54
|
-
)
|
|
55
|
-
article2 = async_to_sync(Article.objects.acreate)(
|
|
56
|
-
title="Article 2",
|
|
57
|
-
content="Content 2",
|
|
58
|
-
author="Author 2",
|
|
59
|
-
)
|
|
60
|
-
|
|
61
|
-
@api.view("/articles", methods=["GET"])
|
|
62
|
-
class ArticleListViewSet(ReadOnlyModelViewSet):
|
|
63
|
-
queryset = Article.objects.all()
|
|
64
|
-
serializer_class = ArticleSchema
|
|
65
|
-
|
|
66
|
-
async def get(self, request):
|
|
67
|
-
"""List all articles."""
|
|
68
|
-
articles = []
|
|
69
|
-
async for article in await self.get_queryset():
|
|
70
|
-
articles.append(ArticleSchema.from_model(article))
|
|
71
|
-
return articles
|
|
72
|
-
|
|
73
|
-
@api.view("/articles/{pk}", methods=["GET"])
|
|
74
|
-
class ArticleDetailViewSet(ReadOnlyModelViewSet):
|
|
75
|
-
queryset = Article.objects.all()
|
|
76
|
-
serializer_class = ArticleSchema
|
|
77
|
-
|
|
78
|
-
async def get(self, request, pk: int):
|
|
79
|
-
"""Retrieve a single article."""
|
|
80
|
-
article = await self.get_object(pk)
|
|
81
|
-
return ArticleSchema.from_model(article)
|
|
82
|
-
|
|
83
|
-
with TestClient(api) as client:
|
|
84
|
-
# List
|
|
85
|
-
response = client.get("/articles")
|
|
86
|
-
assert response.status_code == 200
|
|
87
|
-
data = response.json()
|
|
88
|
-
assert len(data) == 2
|
|
89
|
-
assert all("id" in article and "title" in article for article in data)
|
|
90
|
-
|
|
91
|
-
# Retrieve
|
|
92
|
-
response = client.get(f"/articles/{article1.id}")
|
|
93
|
-
assert response.status_code == 200
|
|
94
|
-
data = response.json()
|
|
95
|
-
assert data["id"] == article1.id
|
|
96
|
-
assert data["title"] == "Article 1"
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
@pytest.mark.django_db(transaction=True)
|
|
100
|
-
def test_model_viewset_with_custom_methods(api):
|
|
101
|
-
"""Test ModelViewSet with full CRUD implementation."""
|
|
102
|
-
|
|
103
|
-
@api.view("/articles", methods=["GET", "POST"])
|
|
104
|
-
class ArticleListViewSet(ModelViewSet):
|
|
105
|
-
queryset = Article.objects.all()
|
|
106
|
-
serializer_class = ArticleSchema
|
|
107
|
-
|
|
108
|
-
async def get(self, request):
|
|
109
|
-
"""List all articles."""
|
|
110
|
-
articles = []
|
|
111
|
-
async for article in await self.get_queryset():
|
|
112
|
-
articles.append(ArticleSchema.from_model(article))
|
|
113
|
-
return articles
|
|
114
|
-
|
|
115
|
-
async def post(self, request, data: ArticleCreateSchema):
|
|
116
|
-
"""Create a new article."""
|
|
117
|
-
article = await Article.objects.acreate(
|
|
118
|
-
title=data.title,
|
|
119
|
-
content=data.content,
|
|
120
|
-
author=data.author,
|
|
121
|
-
)
|
|
122
|
-
return ArticleSchema.from_model(article)
|
|
123
|
-
|
|
124
|
-
@api.view("/articles/{pk}", methods=["GET", "PUT", "PATCH", "DELETE"])
|
|
125
|
-
class ArticleDetailViewSet(ModelViewSet):
|
|
126
|
-
queryset = Article.objects.all()
|
|
127
|
-
serializer_class = ArticleSchema
|
|
128
|
-
|
|
129
|
-
async def get(self, request, pk: int):
|
|
130
|
-
"""Retrieve a single article."""
|
|
131
|
-
article = await self.get_object(pk)
|
|
132
|
-
return ArticleSchema.from_model(article)
|
|
133
|
-
|
|
134
|
-
async def put(self, request, pk: int, data: ArticleCreateSchema):
|
|
135
|
-
"""Update an article."""
|
|
136
|
-
article = await self.get_object(pk)
|
|
137
|
-
article.title = data.title
|
|
138
|
-
article.content = data.content
|
|
139
|
-
article.author = data.author
|
|
140
|
-
await article.asave()
|
|
141
|
-
return ArticleSchema.from_model(article)
|
|
142
|
-
|
|
143
|
-
async def patch(self, request, pk: int, data: ArticleCreateSchema):
|
|
144
|
-
"""Partially update an article."""
|
|
145
|
-
article = await self.get_object(pk)
|
|
146
|
-
if data.title:
|
|
147
|
-
article.title = data.title
|
|
148
|
-
if data.content:
|
|
149
|
-
article.content = data.content
|
|
150
|
-
if data.author:
|
|
151
|
-
article.author = data.author
|
|
152
|
-
await article.asave()
|
|
153
|
-
return ArticleSchema.from_model(article)
|
|
154
|
-
|
|
155
|
-
async def delete(self, request, pk: int):
|
|
156
|
-
"""Delete an article."""
|
|
157
|
-
article = await self.get_object(pk)
|
|
158
|
-
await article.adelete()
|
|
159
|
-
return {"detail": "Object deleted successfully"}
|
|
160
|
-
|
|
161
|
-
with TestClient(api) as client:
|
|
162
|
-
# List
|
|
163
|
-
response = client.get("/articles")
|
|
164
|
-
assert response.status_code == 200
|
|
165
|
-
assert response.json() == []
|
|
166
|
-
|
|
167
|
-
# Create
|
|
168
|
-
response = client.post(
|
|
169
|
-
"/articles",
|
|
170
|
-
json={"title": "New Article", "content": "New Content", "author": "Test Author"},
|
|
171
|
-
)
|
|
172
|
-
assert response.status_code == 200
|
|
173
|
-
article_id = response.json()["id"]
|
|
174
|
-
|
|
175
|
-
# Retrieve
|
|
176
|
-
response = client.get(f"/articles/{article_id}")
|
|
177
|
-
assert response.status_code == 200
|
|
178
|
-
assert response.json()["title"] == "New Article"
|
|
179
|
-
|
|
180
|
-
# Update
|
|
181
|
-
response = client.put(
|
|
182
|
-
f"/articles/{article_id}",
|
|
183
|
-
json={"title": "Updated Title", "content": "Updated Content", "author": "Updated Author"},
|
|
184
|
-
)
|
|
185
|
-
assert response.status_code == 200
|
|
186
|
-
assert response.json()["title"] == "Updated Title"
|
|
187
|
-
|
|
188
|
-
# Partial update
|
|
189
|
-
response = client.patch(
|
|
190
|
-
f"/articles/{article_id}",
|
|
191
|
-
json={"title": "Patched Title", "content": "", "author": ""},
|
|
192
|
-
)
|
|
193
|
-
assert response.status_code == 200
|
|
194
|
-
assert response.json()["title"] == "Patched Title"
|
|
195
|
-
|
|
196
|
-
# Delete
|
|
197
|
-
response = client.delete(f"/articles/{article_id}")
|
|
198
|
-
assert response.status_code == 200
|
|
199
|
-
|
|
200
|
-
# Verify deletion
|
|
201
|
-
response = client.get(f"/articles/{article_id}")
|
|
202
|
-
assert response.status_code == 404
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
@pytest.mark.django_db(transaction=True)
|
|
206
|
-
def test_model_viewset_queryset_reevaluation(api):
|
|
207
|
-
"""Test that queryset is re-evaluated on each request (like DRF)."""
|
|
208
|
-
from asgiref.sync import async_to_sync
|
|
209
|
-
|
|
210
|
-
@api.view("/articles", methods=["GET"])
|
|
211
|
-
class ArticleViewSet(ReadOnlyModelViewSet):
|
|
212
|
-
queryset = Article.objects.all()
|
|
213
|
-
serializer_class = ArticleSchema
|
|
214
|
-
|
|
215
|
-
async def get(self, request):
|
|
216
|
-
"""List all articles."""
|
|
217
|
-
articles = []
|
|
218
|
-
async for article in await self.get_queryset():
|
|
219
|
-
articles.append(ArticleSchema.from_model(article))
|
|
220
|
-
return articles
|
|
221
|
-
|
|
222
|
-
with TestClient(api) as client:
|
|
223
|
-
# First request - empty
|
|
224
|
-
response = client.get("/articles")
|
|
225
|
-
assert response.status_code == 200
|
|
226
|
-
assert len(response.json()) == 0
|
|
227
|
-
|
|
228
|
-
# Create article outside the viewset
|
|
229
|
-
async_to_sync(Article.objects.acreate)(
|
|
230
|
-
title="Article 1",
|
|
231
|
-
content="Content 1",
|
|
232
|
-
author="Author 1",
|
|
233
|
-
)
|
|
234
|
-
|
|
235
|
-
# Second request - should see the new article (queryset re-evaluated)
|
|
236
|
-
response = client.get("/articles")
|
|
237
|
-
assert response.status_code == 200
|
|
238
|
-
assert len(response.json()) == 1
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
@pytest.mark.django_db(transaction=True)
|
|
242
|
-
def test_model_viewset_custom_queryset(api):
|
|
243
|
-
"""Test ModelViewSet with custom get_queryset()."""
|
|
244
|
-
from asgiref.sync import async_to_sync
|
|
245
|
-
|
|
246
|
-
# Create test data
|
|
247
|
-
async_to_sync(Article.objects.acreate)(
|
|
248
|
-
title="Published 1",
|
|
249
|
-
content="Content",
|
|
250
|
-
author="Author",
|
|
251
|
-
is_published=True,
|
|
252
|
-
)
|
|
253
|
-
async_to_sync(Article.objects.acreate)(
|
|
254
|
-
title="Draft 1",
|
|
255
|
-
content="Content",
|
|
256
|
-
author="Author",
|
|
257
|
-
is_published=False,
|
|
258
|
-
)
|
|
259
|
-
|
|
260
|
-
@api.view("/articles/published", methods=["GET"])
|
|
261
|
-
class PublishedArticleViewSet(ReadOnlyModelViewSet):
|
|
262
|
-
queryset = Article.objects.all() # Base queryset
|
|
263
|
-
serializer_class = ArticleSchema
|
|
264
|
-
|
|
265
|
-
async def get_queryset(self):
|
|
266
|
-
# Custom filtering
|
|
267
|
-
queryset = await super().get_queryset()
|
|
268
|
-
return queryset.filter(is_published=True)
|
|
269
|
-
|
|
270
|
-
async def get(self, request):
|
|
271
|
-
"""List published articles."""
|
|
272
|
-
articles = []
|
|
273
|
-
async for article in await self.get_queryset():
|
|
274
|
-
articles.append(ArticleSchema.from_model(article))
|
|
275
|
-
return articles
|
|
276
|
-
|
|
277
|
-
with TestClient(api) as client:
|
|
278
|
-
response = client.get("/articles/published")
|
|
279
|
-
assert response.status_code == 200
|
|
280
|
-
data = response.json()
|
|
281
|
-
# Should only get published articles
|
|
282
|
-
assert len(data) == 1
|
|
283
|
-
assert data[0]["is_published"] is True
|
|
284
|
-
assert data[0]["title"] == "Published 1"
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
@pytest.mark.django_db(transaction=True)
|
|
288
|
-
def test_model_viewset_lookup_field(api):
|
|
289
|
-
"""Test ModelViewSet with custom lookup_field."""
|
|
290
|
-
from asgiref.sync import async_to_sync
|
|
291
|
-
|
|
292
|
-
# Create article
|
|
293
|
-
article = async_to_sync(Article.objects.acreate)(
|
|
294
|
-
title="Test Article",
|
|
295
|
-
content="Content",
|
|
296
|
-
author="test-author",
|
|
297
|
-
)
|
|
298
|
-
|
|
299
|
-
# Use {pk} in URL pattern (will be matched to author field)
|
|
300
|
-
@api.view("/articles/by-author/{pk}", methods=["GET"])
|
|
301
|
-
class ArticleViewSet(ReadOnlyModelViewSet):
|
|
302
|
-
queryset = Article.objects.all()
|
|
303
|
-
serializer_class = ArticleSchema
|
|
304
|
-
lookup_field = 'author' # Look up by author instead of pk
|
|
305
|
-
|
|
306
|
-
async def get(self, request, pk: str): # pk will be the author name
|
|
307
|
-
"""Retrieve article by author."""
|
|
308
|
-
article = await self.get_object(pk)
|
|
309
|
-
return ArticleSchema.from_model(article)
|
|
310
|
-
|
|
311
|
-
with TestClient(api) as client:
|
|
312
|
-
# Lookup by author
|
|
313
|
-
response = client.get("/articles/by-author/test-author")
|
|
314
|
-
assert response.status_code == 200
|
|
315
|
-
data = response.json()
|
|
316
|
-
assert data["author"] == "test-author"
|
|
317
|
-
assert data["title"] == "Test Article"
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
@pytest.fixture
|
|
321
|
-
def api():
|
|
322
|
-
"""Create a fresh BoltAPI instance for each test."""
|
|
323
|
-
return BoltAPI()
|
django_bolt/tests/test_models.py
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Test models for Django ORM integration tests.
|
|
3
|
-
|
|
4
|
-
These models are used to verify that ViewSets work with real Django ORM operations.
|
|
5
|
-
"""
|
|
6
|
-
from django.db import models
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
class Article(models.Model):
|
|
10
|
-
"""Test model for ViewSet/Mixin Django ORM integration tests."""
|
|
11
|
-
|
|
12
|
-
title = models.CharField(max_length=200)
|
|
13
|
-
content = models.TextField()
|
|
14
|
-
author = models.CharField(max_length=100)
|
|
15
|
-
is_published = models.BooleanField(default=False)
|
|
16
|
-
created_at = models.DateTimeField(auto_now_add=True)
|
|
17
|
-
updated_at = models.DateTimeField(auto_now=True)
|
|
18
|
-
|
|
19
|
-
class Meta:
|
|
20
|
-
app_label = 'django_bolt'
|
|
21
|
-
ordering = ['-created_at']
|
|
22
|
-
|
|
23
|
-
def __str__(self):
|
|
24
|
-
return self.title
|