django-bolt 0.1.0__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.

Files changed (128) hide show
  1. django_bolt/__init__.py +147 -0
  2. django_bolt/_core.pyd +0 -0
  3. django_bolt/admin/__init__.py +25 -0
  4. django_bolt/admin/admin_detection.py +179 -0
  5. django_bolt/admin/asgi_bridge.py +267 -0
  6. django_bolt/admin/routes.py +91 -0
  7. django_bolt/admin/static.py +155 -0
  8. django_bolt/admin/static_routes.py +111 -0
  9. django_bolt/api.py +1011 -0
  10. django_bolt/apps.py +7 -0
  11. django_bolt/async_collector.py +228 -0
  12. django_bolt/auth/README.md +464 -0
  13. django_bolt/auth/REVOCATION_EXAMPLE.md +391 -0
  14. django_bolt/auth/__init__.py +84 -0
  15. django_bolt/auth/backends.py +236 -0
  16. django_bolt/auth/guards.py +224 -0
  17. django_bolt/auth/jwt_utils.py +212 -0
  18. django_bolt/auth/revocation.py +286 -0
  19. django_bolt/auth/token.py +335 -0
  20. django_bolt/binding.py +363 -0
  21. django_bolt/bootstrap.py +77 -0
  22. django_bolt/cli.py +133 -0
  23. django_bolt/compression.py +104 -0
  24. django_bolt/decorators.py +159 -0
  25. django_bolt/dependencies.py +128 -0
  26. django_bolt/error_handlers.py +305 -0
  27. django_bolt/exceptions.py +294 -0
  28. django_bolt/health.py +129 -0
  29. django_bolt/logging/__init__.py +6 -0
  30. django_bolt/logging/config.py +357 -0
  31. django_bolt/logging/middleware.py +296 -0
  32. django_bolt/management/__init__.py +1 -0
  33. django_bolt/management/commands/__init__.py +0 -0
  34. django_bolt/management/commands/runbolt.py +427 -0
  35. django_bolt/middleware/__init__.py +32 -0
  36. django_bolt/middleware/compiler.py +131 -0
  37. django_bolt/middleware/middleware.py +247 -0
  38. django_bolt/openapi/__init__.py +23 -0
  39. django_bolt/openapi/config.py +196 -0
  40. django_bolt/openapi/plugins.py +439 -0
  41. django_bolt/openapi/routes.py +152 -0
  42. django_bolt/openapi/schema_generator.py +581 -0
  43. django_bolt/openapi/spec/__init__.py +68 -0
  44. django_bolt/openapi/spec/base.py +74 -0
  45. django_bolt/openapi/spec/callback.py +24 -0
  46. django_bolt/openapi/spec/components.py +72 -0
  47. django_bolt/openapi/spec/contact.py +21 -0
  48. django_bolt/openapi/spec/discriminator.py +25 -0
  49. django_bolt/openapi/spec/encoding.py +67 -0
  50. django_bolt/openapi/spec/enums.py +41 -0
  51. django_bolt/openapi/spec/example.py +36 -0
  52. django_bolt/openapi/spec/external_documentation.py +21 -0
  53. django_bolt/openapi/spec/header.py +132 -0
  54. django_bolt/openapi/spec/info.py +50 -0
  55. django_bolt/openapi/spec/license.py +28 -0
  56. django_bolt/openapi/spec/link.py +66 -0
  57. django_bolt/openapi/spec/media_type.py +51 -0
  58. django_bolt/openapi/spec/oauth_flow.py +36 -0
  59. django_bolt/openapi/spec/oauth_flows.py +28 -0
  60. django_bolt/openapi/spec/open_api.py +87 -0
  61. django_bolt/openapi/spec/operation.py +105 -0
  62. django_bolt/openapi/spec/parameter.py +147 -0
  63. django_bolt/openapi/spec/path_item.py +78 -0
  64. django_bolt/openapi/spec/paths.py +27 -0
  65. django_bolt/openapi/spec/reference.py +38 -0
  66. django_bolt/openapi/spec/request_body.py +38 -0
  67. django_bolt/openapi/spec/response.py +48 -0
  68. django_bolt/openapi/spec/responses.py +44 -0
  69. django_bolt/openapi/spec/schema.py +678 -0
  70. django_bolt/openapi/spec/security_requirement.py +28 -0
  71. django_bolt/openapi/spec/security_scheme.py +69 -0
  72. django_bolt/openapi/spec/server.py +34 -0
  73. django_bolt/openapi/spec/server_variable.py +32 -0
  74. django_bolt/openapi/spec/tag.py +32 -0
  75. django_bolt/openapi/spec/xml.py +44 -0
  76. django_bolt/pagination.py +669 -0
  77. django_bolt/param_functions.py +49 -0
  78. django_bolt/params.py +337 -0
  79. django_bolt/request_parsing.py +128 -0
  80. django_bolt/responses.py +214 -0
  81. django_bolt/router.py +48 -0
  82. django_bolt/serialization.py +193 -0
  83. django_bolt/status_codes.py +321 -0
  84. django_bolt/testing/__init__.py +10 -0
  85. django_bolt/testing/client.py +274 -0
  86. django_bolt/testing/helpers.py +93 -0
  87. django_bolt/tests/__init__.py +0 -0
  88. django_bolt/tests/admin_tests/__init__.py +1 -0
  89. django_bolt/tests/admin_tests/conftest.py +6 -0
  90. django_bolt/tests/admin_tests/test_admin_with_django.py +278 -0
  91. django_bolt/tests/admin_tests/urls.py +9 -0
  92. django_bolt/tests/cbv/__init__.py +0 -0
  93. django_bolt/tests/cbv/test_class_views.py +570 -0
  94. django_bolt/tests/cbv/test_class_views_django_orm.py +703 -0
  95. django_bolt/tests/cbv/test_class_views_features.py +1173 -0
  96. django_bolt/tests/cbv/test_class_views_with_client.py +622 -0
  97. django_bolt/tests/conftest.py +165 -0
  98. django_bolt/tests/test_action_decorator.py +399 -0
  99. django_bolt/tests/test_auth_secret_key.py +83 -0
  100. django_bolt/tests/test_decorator_syntax.py +159 -0
  101. django_bolt/tests/test_error_handling.py +481 -0
  102. django_bolt/tests/test_file_response.py +192 -0
  103. django_bolt/tests/test_global_cors.py +172 -0
  104. django_bolt/tests/test_guards_auth.py +441 -0
  105. django_bolt/tests/test_guards_integration.py +303 -0
  106. django_bolt/tests/test_health.py +283 -0
  107. django_bolt/tests/test_integration_validation.py +400 -0
  108. django_bolt/tests/test_json_validation.py +536 -0
  109. django_bolt/tests/test_jwt_auth.py +327 -0
  110. django_bolt/tests/test_jwt_token.py +458 -0
  111. django_bolt/tests/test_logging.py +837 -0
  112. django_bolt/tests/test_logging_merge.py +419 -0
  113. django_bolt/tests/test_middleware.py +492 -0
  114. django_bolt/tests/test_middleware_server.py +230 -0
  115. django_bolt/tests/test_model_viewset.py +323 -0
  116. django_bolt/tests/test_models.py +24 -0
  117. django_bolt/tests/test_pagination.py +1258 -0
  118. django_bolt/tests/test_parameter_validation.py +178 -0
  119. django_bolt/tests/test_syntax.py +626 -0
  120. django_bolt/tests/test_testing_utilities.py +163 -0
  121. django_bolt/tests/test_testing_utilities_simple.py +123 -0
  122. django_bolt/tests/test_viewset_unified.py +346 -0
  123. django_bolt/typing.py +273 -0
  124. django_bolt/views.py +1110 -0
  125. django_bolt-0.1.0.dist-info/METADATA +629 -0
  126. django_bolt-0.1.0.dist-info/RECORD +128 -0
  127. django_bolt-0.1.0.dist-info/WHEEL +4 -0
  128. django_bolt-0.1.0.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,230 @@
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
@@ -0,0 +1,323 @@
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()
@@ -0,0 +1,24 @@
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