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,1258 @@
1
+ """
2
+ Real ORM-based tests for pagination functionality.
3
+
4
+ Tests pagination with actual Django models, database queries, and HTTP requests.
5
+ No mocking - tests the full integration stack.
6
+ """
7
+ import pytest
8
+ import msgspec
9
+ from typing import List
10
+
11
+ from django_bolt import (
12
+ BoltAPI,
13
+ ViewSet,
14
+ ModelViewSet,
15
+ PageNumberPagination,
16
+ LimitOffsetPagination,
17
+ CursorPagination,
18
+ paginate,
19
+ )
20
+ from django_bolt.testing import TestClient
21
+ from django_bolt.tests.test_models import Article
22
+
23
+
24
+ # ============================================================================
25
+ # Schemas
26
+ # ============================================================================
27
+
28
+ class ArticleSchema(msgspec.Struct):
29
+ id: int
30
+ title: str
31
+ content: str
32
+ author: str
33
+ is_published: bool
34
+
35
+
36
+ class ArticleListSchema(msgspec.Struct):
37
+ id: int
38
+ title: str
39
+ author: str
40
+
41
+
42
+ # ============================================================================
43
+ # Test Fixtures
44
+ # ============================================================================
45
+
46
+ @pytest.fixture
47
+ def sample_articles(db):
48
+ """Create sample articles in the database"""
49
+ articles = []
50
+ for i in range(1, 51): # Create 50 articles
51
+ article = Article.objects.create(
52
+ title=f"Article {i}",
53
+ content=f"Content for article {i}",
54
+ author=f"Author {i % 10}",
55
+ is_published=i % 2 == 0, # Half published, half not
56
+ )
57
+ articles.append(article)
58
+ return articles
59
+
60
+
61
+ # ============================================================================
62
+ # PageNumberPagination Tests
63
+ # ============================================================================
64
+
65
+ @pytest.mark.django_db(transaction=True)
66
+ def test_page_number_pagination_first_page(sample_articles):
67
+ """Test PageNumberPagination first page with real ORM"""
68
+ api = BoltAPI()
69
+
70
+ class SmallPagePagination(PageNumberPagination):
71
+ page_size = 10
72
+
73
+ @api.get("/articles")
74
+ @paginate(SmallPagePagination)
75
+ async def list_articles(request):
76
+ return Article.objects.all()
77
+
78
+ with TestClient(api) as client:
79
+ response = client.get("/articles?page=1")
80
+ assert response.status_code == 200
81
+
82
+ data = response.json()
83
+ assert "items" in data
84
+ assert "total" in data
85
+ assert len(data["items"]) == 10
86
+ assert data["total"] == 50
87
+ assert data["page"] == 1
88
+ assert data["page_size"] == 10
89
+ assert data["total_pages"] == 5
90
+ assert data["has_next"] is True
91
+ assert data["has_previous"] is False
92
+ assert data["next_page"] == 2
93
+ assert data["previous_page"] is None
94
+
95
+ # Check first item (ordered by -created_at)
96
+ assert data["items"][0]["title"] == "Article 50"
97
+
98
+
99
+ @pytest.mark.django_db(transaction=True)
100
+ def test_page_number_pagination_middle_page(sample_articles):
101
+ """Test pagination on middle page"""
102
+ api = BoltAPI()
103
+
104
+ class SmallPagePagination(PageNumberPagination):
105
+ page_size = 10
106
+
107
+ @api.get("/articles")
108
+ @paginate(SmallPagePagination)
109
+ async def list_articles(request):
110
+ return Article.objects.all()
111
+
112
+ with TestClient(api) as client:
113
+ response = client.get("/articles?page=3")
114
+ assert response.status_code == 200
115
+
116
+ data = response.json()
117
+ assert data["page"] == 3
118
+ assert len(data["items"]) == 10
119
+ assert data["has_next"] is True
120
+ assert data["has_previous"] is True
121
+ assert data["next_page"] == 4
122
+ assert data["previous_page"] == 2
123
+
124
+
125
+ @pytest.mark.django_db(transaction=True)
126
+ def test_page_number_pagination_last_page(sample_articles):
127
+ """Test pagination on last page"""
128
+ api = BoltAPI()
129
+
130
+ class SmallPagePagination(PageNumberPagination):
131
+ page_size = 10
132
+
133
+ @api.get("/articles")
134
+ @paginate(SmallPagePagination)
135
+ async def list_articles(request):
136
+ return Article.objects.all()
137
+
138
+ with TestClient(api) as client:
139
+ response = client.get("/articles?page=5")
140
+ assert response.status_code == 200
141
+
142
+ data = response.json()
143
+ assert data["page"] == 5
144
+ assert len(data["items"]) == 10
145
+ assert data["has_next"] is False
146
+ assert data["has_previous"] is True
147
+ assert data["next_page"] is None
148
+ assert data["previous_page"] == 4
149
+
150
+
151
+ @pytest.mark.django_db(transaction=True)
152
+ def test_page_number_pagination_custom_page_size(sample_articles):
153
+ """Test pagination with custom page size via query param"""
154
+ api = BoltAPI()
155
+
156
+ class CustomPagination(PageNumberPagination):
157
+ page_size = 10
158
+ page_size_query_param = "page_size"
159
+
160
+ @api.get("/articles")
161
+ @paginate(CustomPagination)
162
+ async def list_articles(request):
163
+ return Article.objects.all()
164
+
165
+ with TestClient(api) as client:
166
+ response = client.get("/articles?page=1&page_size=20")
167
+ assert response.status_code == 200
168
+
169
+ data = response.json()
170
+ assert len(data["items"]) == 20
171
+ assert data["page_size"] == 20
172
+ assert data["total_pages"] == 3
173
+
174
+
175
+ @pytest.mark.django_db(transaction=True)
176
+ def test_page_number_pagination_empty_results(db):
177
+ """Test pagination with no results"""
178
+ api = BoltAPI()
179
+
180
+ @api.get("/articles")
181
+ @paginate(PageNumberPagination)
182
+ async def list_articles(request):
183
+ return Article.objects.all()
184
+
185
+ with TestClient(api) as client:
186
+ response = client.get("/articles?page=1")
187
+ assert response.status_code == 200
188
+
189
+ data = response.json()
190
+ assert len(data["items"]) == 0
191
+ assert data["total"] == 0
192
+ assert data["total_pages"] == 0
193
+ assert data["has_next"] is False
194
+ assert data["has_previous"] is False
195
+
196
+
197
+ @pytest.mark.django_db(transaction=True)
198
+ def test_page_number_pagination_with_filtering(sample_articles):
199
+ """Test pagination with queryset filtering"""
200
+ api = BoltAPI()
201
+
202
+ class SmallPagePagination(PageNumberPagination):
203
+ page_size = 10
204
+
205
+ @api.get("/articles")
206
+ @paginate(SmallPagePagination)
207
+ async def list_articles(request, is_published: bool = None):
208
+ qs = Article.objects.all()
209
+ if is_published is not None:
210
+ qs = qs.filter(is_published=is_published)
211
+ return qs
212
+
213
+ with TestClient(api) as client:
214
+ response = client.get("/articles?is_published=true&page=1")
215
+ assert response.status_code == 200
216
+
217
+ data = response.json()
218
+ assert data["total"] == 25 # Half are published
219
+ assert len(data["items"]) == 10
220
+
221
+
222
+ @pytest.mark.django_db(transaction=True)
223
+ def test_page_number_pagination_with_ordering(sample_articles):
224
+ """Test pagination respects queryset ordering"""
225
+ api = BoltAPI()
226
+
227
+ class SmallPagePagination(PageNumberPagination):
228
+ page_size = 10
229
+
230
+ @api.get("/articles")
231
+ @paginate(SmallPagePagination)
232
+ async def list_articles(request):
233
+ return Article.objects.order_by("title")
234
+
235
+ with TestClient(api) as client:
236
+ response = client.get("/articles?page=1")
237
+ assert response.status_code == 200
238
+
239
+ data = response.json()
240
+ # Alphabetical ordering: Article 1, Article 10, Article 11, ..., Article 19, Article 2, ...
241
+ assert data["items"][0]["title"] == "Article 1"
242
+ assert data["items"][1]["title"] == "Article 10"
243
+
244
+
245
+ # ============================================================================
246
+ # LimitOffsetPagination Tests
247
+ # ============================================================================
248
+
249
+ @pytest.mark.django_db(transaction=True)
250
+ def test_limit_offset_pagination_basic(sample_articles):
251
+ """Test LimitOffsetPagination with real ORM"""
252
+ api = BoltAPI()
253
+
254
+ @api.get("/articles")
255
+ @paginate(LimitOffsetPagination)
256
+ async def list_articles(request):
257
+ return Article.objects.all()
258
+
259
+ with TestClient(api) as client:
260
+ response = client.get("/articles?limit=10&offset=0")
261
+ assert response.status_code == 200
262
+
263
+ data = response.json()
264
+ assert len(data["items"]) == 10
265
+ assert data["total"] == 50
266
+ assert data["limit"] == 10
267
+ assert data["offset"] == 0
268
+ assert data["has_next"] is True
269
+ assert data["has_previous"] is False
270
+
271
+
272
+ @pytest.mark.django_db(transaction=True)
273
+ def test_limit_offset_pagination_with_offset(sample_articles):
274
+ """Test limit-offset with specific offset"""
275
+ api = BoltAPI()
276
+
277
+ @api.get("/articles")
278
+ @paginate(LimitOffsetPagination)
279
+ async def list_articles(request):
280
+ return Article.objects.all()
281
+
282
+ with TestClient(api) as client:
283
+ response = client.get("/articles?limit=10&offset=20")
284
+ assert response.status_code == 200
285
+
286
+ data = response.json()
287
+ assert len(data["items"]) == 10
288
+ assert data["limit"] == 10
289
+ assert data["offset"] == 20
290
+ assert data["has_next"] is True
291
+ assert data["has_previous"] is True
292
+
293
+
294
+ @pytest.mark.django_db(transaction=True)
295
+ def test_limit_offset_pagination_last_page(sample_articles):
296
+ """Test limit-offset on last page"""
297
+ api = BoltAPI()
298
+
299
+ @api.get("/articles")
300
+ @paginate(LimitOffsetPagination)
301
+ async def list_articles(request):
302
+ return Article.objects.all()
303
+
304
+ with TestClient(api) as client:
305
+ response = client.get("/articles?limit=10&offset=45")
306
+ assert response.status_code == 200
307
+
308
+ data = response.json()
309
+ assert len(data["items"]) == 5 # Only 5 items left
310
+ assert data["has_next"] is False
311
+ assert data["has_previous"] is True
312
+
313
+
314
+ @pytest.mark.django_db(transaction=True)
315
+ def test_limit_offset_pagination_default_limit(sample_articles):
316
+ """Test default limit when not specified"""
317
+ api = BoltAPI()
318
+
319
+ class CustomLimitOffset(LimitOffsetPagination):
320
+ page_size = 15
321
+
322
+ @api.get("/articles")
323
+ @paginate(CustomLimitOffset)
324
+ async def list_articles(request):
325
+ return Article.objects.all()
326
+
327
+ with TestClient(api) as client:
328
+ response = client.get("/articles")
329
+ assert response.status_code == 200
330
+
331
+ data = response.json()
332
+ assert len(data["items"]) == 15
333
+ assert data["limit"] == 15
334
+ assert data["offset"] == 0
335
+
336
+
337
+ @pytest.mark.django_db(transaction=True)
338
+ def test_limit_offset_pagination_max_limit_enforcement(sample_articles):
339
+ """Test that max_page_size is enforced"""
340
+ api = BoltAPI()
341
+
342
+ class LimitedOffsetPagination(LimitOffsetPagination):
343
+ page_size = 10
344
+ max_page_size = 25
345
+
346
+ @api.get("/articles")
347
+ @paginate(LimitedOffsetPagination)
348
+ async def list_articles(request):
349
+ return Article.objects.all()
350
+
351
+ with TestClient(api) as client:
352
+ response = client.get("/articles?limit=100&offset=0")
353
+ assert response.status_code == 200
354
+
355
+ data = response.json()
356
+ assert data["limit"] == 25 # Clamped to max
357
+ assert len(data["items"]) == 25
358
+
359
+
360
+ @pytest.mark.django_db(transaction=True)
361
+ def test_limit_offset_pagination_with_filtering(sample_articles):
362
+ """Test limit-offset pagination with filtering"""
363
+ api = BoltAPI()
364
+
365
+ @api.get("/articles")
366
+ @paginate(LimitOffsetPagination)
367
+ async def list_articles(request, author: str = None):
368
+ qs = Article.objects.all()
369
+ if author:
370
+ qs = qs.filter(author=author)
371
+ return qs
372
+
373
+ with TestClient(api) as client:
374
+ response = client.get("/articles?limit=10&offset=0&author=Author 1")
375
+ assert response.status_code == 200
376
+
377
+ data = response.json()
378
+ # Author 1 appears at indices: 1, 11, 21, 31, 41 (5 total)
379
+ assert data["total"] == 5
380
+ assert len(data["items"]) == 5
381
+ assert all(item["author"] == "Author 1" for item in data["items"])
382
+
383
+
384
+ @pytest.mark.django_db(transaction=True)
385
+ def test_limit_offset_pagination_with_ordering(sample_articles):
386
+ """Test limit-offset respects queryset ordering"""
387
+ api = BoltAPI()
388
+
389
+ @api.get("/articles")
390
+ @paginate(LimitOffsetPagination)
391
+ async def list_articles(request):
392
+ return Article.objects.order_by("id")
393
+
394
+ with TestClient(api) as client:
395
+ response = client.get("/articles?limit=5&offset=0")
396
+ assert response.status_code == 200
397
+
398
+ data = response.json()
399
+ # Should get Article 1-5 in ascending ID order
400
+ assert data["items"][0]["title"] == "Article 1"
401
+ assert data["items"][4]["title"] == "Article 5"
402
+
403
+
404
+ @pytest.mark.django_db(transaction=True)
405
+ def test_limit_offset_pagination_empty_results(db):
406
+ """Test limit-offset with no results"""
407
+ api = BoltAPI()
408
+
409
+ @api.get("/articles")
410
+ @paginate(LimitOffsetPagination)
411
+ async def list_articles(request):
412
+ return Article.objects.all()
413
+
414
+ with TestClient(api) as client:
415
+ response = client.get("/articles?limit=10&offset=0")
416
+ assert response.status_code == 200
417
+
418
+ data = response.json()
419
+ assert len(data["items"]) == 0
420
+ assert data["total"] == 0
421
+ assert data["has_next"] is False
422
+ assert data["has_previous"] is False
423
+
424
+
425
+ @pytest.mark.django_db(transaction=True)
426
+ def test_limit_offset_pagination_offset_beyond_total(sample_articles):
427
+ """Test offset beyond total results"""
428
+ api = BoltAPI()
429
+
430
+ @api.get("/articles")
431
+ @paginate(LimitOffsetPagination)
432
+ async def list_articles(request):
433
+ return Article.objects.all()
434
+
435
+ with TestClient(api) as client:
436
+ response = client.get("/articles?limit=10&offset=100")
437
+ assert response.status_code == 200
438
+
439
+ data = response.json()
440
+ assert len(data["items"]) == 0
441
+ assert data["total"] == 50
442
+ assert data["has_next"] is False
443
+ assert data["has_previous"] is True
444
+
445
+
446
+ # ============================================================================
447
+ # CursorPagination Tests
448
+ # ============================================================================
449
+
450
+ @pytest.mark.django_db(transaction=True)
451
+ def test_cursor_pagination_first_page(sample_articles):
452
+ """Test cursor pagination first page"""
453
+ api = BoltAPI()
454
+
455
+ class SmallCursorPagination(CursorPagination):
456
+ page_size = 10
457
+ ordering = "-id"
458
+
459
+ @api.get("/articles")
460
+ @paginate(SmallCursorPagination)
461
+ async def list_articles(request):
462
+ return Article.objects.all()
463
+
464
+ with TestClient(api) as client:
465
+ response = client.get("/articles")
466
+ assert response.status_code == 200
467
+
468
+ data = response.json()
469
+ assert len(data["items"]) == 10
470
+ assert data["page_size"] == 10
471
+ assert data["has_next"] is True
472
+ assert data["has_previous"] is False
473
+ assert data["next_cursor"] is not None
474
+
475
+
476
+ @pytest.mark.django_db(transaction=True)
477
+ def test_cursor_pagination_with_cursor(sample_articles):
478
+ """Test cursor pagination with cursor value"""
479
+ api = BoltAPI()
480
+
481
+ class SmallCursorPagination(CursorPagination):
482
+ page_size = 10
483
+ ordering = "-id"
484
+
485
+ @api.get("/articles")
486
+ @paginate(SmallCursorPagination)
487
+ async def list_articles(request):
488
+ return Article.objects.all()
489
+
490
+ with TestClient(api) as client:
491
+ # Get first page
492
+ response1 = client.get("/articles")
493
+ assert response1.status_code == 200
494
+ data1 = response1.json()
495
+ next_cursor = data1["next_cursor"]
496
+
497
+ # Get second page using cursor
498
+ response2 = client.get(f"/articles?cursor={next_cursor}")
499
+ assert response2.status_code == 200
500
+
501
+ data2 = response2.json()
502
+ assert len(data2["items"]) == 10
503
+ assert data2["has_previous"] is True
504
+
505
+ # Ensure items are different
506
+ items1_ids = {item["id"] for item in data1["items"]}
507
+ items2_ids = {item["id"] for item in data2["items"]}
508
+ assert items1_ids.isdisjoint(items2_ids)
509
+
510
+
511
+ @pytest.mark.django_db(transaction=True)
512
+ def test_cursor_pagination_ascending_order(sample_articles):
513
+ """Test cursor pagination with ascending order"""
514
+ api = BoltAPI()
515
+
516
+ class AscendingCursorPagination(CursorPagination):
517
+ page_size = 10
518
+ ordering = "id" # Ascending
519
+
520
+ @api.get("/articles")
521
+ @paginate(AscendingCursorPagination)
522
+ async def list_articles(request):
523
+ return Article.objects.all()
524
+
525
+ with TestClient(api) as client:
526
+ response = client.get("/articles")
527
+ assert response.status_code == 200
528
+
529
+ data = response.json()
530
+ assert len(data["items"]) == 10
531
+ # First item should have lowest ID
532
+ assert data["items"][0]["title"] == "Article 1"
533
+
534
+
535
+ @pytest.mark.django_db(transaction=True)
536
+ def test_cursor_pagination_last_page(sample_articles):
537
+ """Test cursor pagination on last page"""
538
+ api = BoltAPI()
539
+
540
+ class SmallCursorPagination(CursorPagination):
541
+ page_size = 10
542
+ ordering = "-id"
543
+
544
+ @api.get("/articles")
545
+ @paginate(SmallCursorPagination)
546
+ async def list_articles(request):
547
+ return Article.objects.all()
548
+
549
+ with TestClient(api) as client:
550
+ # Navigate through pages to get to last
551
+ cursor = None
552
+ page_count = 0
553
+
554
+ while True:
555
+ url = "/articles" if cursor is None else f"/articles?cursor={cursor}"
556
+ response = client.get(url)
557
+ assert response.status_code == 200
558
+
559
+ data = response.json()
560
+ page_count += 1
561
+
562
+ if not data["has_next"]:
563
+ # Last page
564
+ assert data["has_previous"] is True
565
+ assert data["next_cursor"] is None
566
+ break
567
+
568
+ cursor = data["next_cursor"]
569
+
570
+ # Safety check to prevent infinite loop
571
+ if page_count > 10:
572
+ break
573
+
574
+ assert page_count == 5 # 50 items / 10 per page
575
+
576
+
577
+ @pytest.mark.django_db(transaction=True)
578
+ def test_cursor_pagination_with_filtering(sample_articles):
579
+ """Test cursor pagination with filtering"""
580
+ api = BoltAPI()
581
+
582
+ class SmallCursorPagination(CursorPagination):
583
+ page_size = 3
584
+ ordering = "-id"
585
+
586
+ @api.get("/articles")
587
+ @paginate(SmallCursorPagination)
588
+ async def list_articles(request, is_published: bool = None):
589
+ qs = Article.objects.all()
590
+ if is_published is not None:
591
+ qs = qs.filter(is_published=is_published)
592
+ return qs
593
+
594
+ with TestClient(api) as client:
595
+ response = client.get("/articles?is_published=true")
596
+ assert response.status_code == 200
597
+
598
+ data = response.json()
599
+ assert len(data["items"]) == 3
600
+ assert all(item["is_published"] for item in data["items"])
601
+
602
+
603
+ @pytest.mark.django_db(transaction=True)
604
+ def test_cursor_pagination_custom_page_size(sample_articles):
605
+ """Test cursor pagination with custom page size"""
606
+ api = BoltAPI()
607
+
608
+ class CustomCursorPagination(CursorPagination):
609
+ page_size = 10
610
+ page_size_query_param = "page_size"
611
+ max_page_size = 30
612
+ ordering = "-id"
613
+
614
+ @api.get("/articles")
615
+ @paginate(CustomCursorPagination)
616
+ async def list_articles(request):
617
+ return Article.objects.all()
618
+
619
+ with TestClient(api) as client:
620
+ response = client.get("/articles?page_size=20")
621
+ assert response.status_code == 200
622
+
623
+ data = response.json()
624
+ assert len(data["items"]) == 20
625
+ assert data["page_size"] == 20
626
+
627
+
628
+ @pytest.mark.django_db(transaction=True)
629
+ def test_cursor_pagination_max_page_size_enforcement(sample_articles):
630
+ """Test cursor pagination enforces max page size"""
631
+ api = BoltAPI()
632
+
633
+ class LimitedCursorPagination(CursorPagination):
634
+ page_size = 10
635
+ page_size_query_param = "page_size"
636
+ max_page_size = 15
637
+ ordering = "-id"
638
+
639
+ @api.get("/articles")
640
+ @paginate(LimitedCursorPagination)
641
+ async def list_articles(request):
642
+ return Article.objects.all()
643
+
644
+ with TestClient(api) as client:
645
+ response = client.get("/articles?page_size=50")
646
+ assert response.status_code == 200
647
+
648
+ data = response.json()
649
+ assert data["page_size"] == 15 # Clamped to max
650
+ assert len(data["items"]) == 15
651
+
652
+
653
+ @pytest.mark.django_db(transaction=True)
654
+ def test_cursor_pagination_empty_results(db):
655
+ """Test cursor pagination with no results"""
656
+ api = BoltAPI()
657
+
658
+ class SmallCursorPagination(CursorPagination):
659
+ page_size = 10
660
+ ordering = "-id"
661
+
662
+ @api.get("/articles")
663
+ @paginate(SmallCursorPagination)
664
+ async def list_articles(request):
665
+ return Article.objects.all()
666
+
667
+ with TestClient(api) as client:
668
+ response = client.get("/articles")
669
+ assert response.status_code == 200
670
+
671
+ data = response.json()
672
+ assert len(data["items"]) == 0
673
+ assert data["has_next"] is False
674
+ assert data["has_previous"] is False
675
+ assert data["next_cursor"] is None
676
+
677
+
678
+ @pytest.mark.django_db(transaction=True)
679
+ def test_cursor_pagination_ordering_by_title(sample_articles):
680
+ """Test cursor pagination with custom ordering field"""
681
+ api = BoltAPI()
682
+
683
+ class TitleOrderedPagination(CursorPagination):
684
+ page_size = 10
685
+ ordering = "title" # Order by title
686
+
687
+ @api.get("/articles")
688
+ @paginate(TitleOrderedPagination)
689
+ async def list_articles(request):
690
+ return Article.objects.all()
691
+
692
+ with TestClient(api) as client:
693
+ response = client.get("/articles")
694
+ assert response.status_code == 200
695
+
696
+ data = response.json()
697
+ assert len(data["items"]) == 10
698
+ # First item should be "Article 1" (alphabetically first)
699
+ assert data["items"][0]["title"] == "Article 1"
700
+
701
+
702
+ @pytest.mark.django_db(transaction=True)
703
+ def test_cursor_pagination_multiple_pages_continuity(sample_articles):
704
+ """Test that cursor pagination maintains continuity across pages"""
705
+ api = BoltAPI()
706
+
707
+ class SmallCursorPagination(CursorPagination):
708
+ page_size = 10
709
+ ordering = "-id"
710
+
711
+ @api.get("/articles")
712
+ @paginate(SmallCursorPagination)
713
+ async def list_articles(request):
714
+ return Article.objects.all()
715
+
716
+ with TestClient(api) as client:
717
+ # Get first page
718
+ response1 = client.get("/articles")
719
+ data1 = response1.json()
720
+
721
+ # Get second page
722
+ response2 = client.get(f"/articles?cursor={data1['next_cursor']}")
723
+ data2 = response2.json()
724
+
725
+ # Get third page
726
+ response3 = client.get(f"/articles?cursor={data2['next_cursor']}")
727
+ data3 = response3.json()
728
+
729
+ # Collect all IDs
730
+ all_ids = []
731
+ all_ids.extend([item["id"] for item in data1["items"]])
732
+ all_ids.extend([item["id"] for item in data2["items"]])
733
+ all_ids.extend([item["id"] for item in data3["items"]])
734
+
735
+ # Ensure no duplicates
736
+ assert len(all_ids) == len(set(all_ids))
737
+
738
+ # Ensure ordering is maintained (descending IDs)
739
+ assert all_ids == sorted(all_ids, reverse=True)
740
+
741
+
742
+ # ============================================================================
743
+ # ViewSet Integration Tests
744
+ # ============================================================================
745
+
746
+ @pytest.mark.django_db(transaction=True)
747
+ def test_viewset_with_pagination(sample_articles):
748
+ """Test ViewSet with @paginate decorator"""
749
+ api = BoltAPI()
750
+
751
+ class SmallPagePagination(PageNumberPagination):
752
+ page_size = 10
753
+
754
+ # Use @paginate decorator on ViewSet method
755
+ @api.viewset("/articles")
756
+ class ArticleViewSet(ViewSet):
757
+ queryset = Article.objects.all()
758
+
759
+ @paginate(SmallPagePagination)
760
+ async def list(self, request):
761
+ return await self.get_queryset()
762
+
763
+ with TestClient(api) as client:
764
+ response = client.get("/articles?page=2")
765
+ assert response.status_code == 200
766
+
767
+ data = response.json()
768
+ assert data["page"] == 2
769
+ assert len(data["items"]) == 10
770
+ assert data["total"] == 50
771
+
772
+
773
+ @pytest.mark.django_db(transaction=True)
774
+ def test_viewset_with_limit_offset_pagination(sample_articles):
775
+ """Test ViewSet with LimitOffsetPagination"""
776
+ api = BoltAPI()
777
+
778
+ @api.viewset("/articles")
779
+ class ArticleViewSet(ViewSet):
780
+ queryset = Article.objects.all()
781
+
782
+ @paginate(LimitOffsetPagination)
783
+ async def list(self, request):
784
+ return await self.get_queryset()
785
+
786
+ with TestClient(api) as client:
787
+ response = client.get("/articles?limit=15&offset=10")
788
+ assert response.status_code == 200
789
+
790
+ data = response.json()
791
+ assert data["limit"] == 15
792
+ assert data["offset"] == 10
793
+ assert len(data["items"]) == 15
794
+
795
+
796
+ @pytest.mark.django_db(transaction=True)
797
+ def test_viewset_with_cursor_pagination(sample_articles):
798
+ """Test ViewSet with CursorPagination"""
799
+ api = BoltAPI()
800
+
801
+ class SmallCursorPagination(CursorPagination):
802
+ page_size = 10
803
+ ordering = "-id"
804
+
805
+ @api.viewset("/articles")
806
+ class ArticleViewSet(ViewSet):
807
+ queryset = Article.objects.all()
808
+
809
+ @paginate(SmallCursorPagination)
810
+ async def list(self, request):
811
+ return await self.get_queryset()
812
+
813
+ with TestClient(api) as client:
814
+ response = client.get("/articles")
815
+ assert response.status_code == 200
816
+
817
+ data = response.json()
818
+ assert len(data["items"]) == 10
819
+ assert data["has_next"] is True
820
+ assert data["next_cursor"] is not None
821
+
822
+
823
+ @pytest.mark.django_db(transaction=True)
824
+ def test_viewset_without_pagination(sample_articles):
825
+ """Test ViewSet without pagination returns all results"""
826
+ api = BoltAPI()
827
+
828
+ @api.viewset("/articles")
829
+ class ArticleViewSet(ViewSet):
830
+ queryset = Article.objects.all()
831
+ pagination_class = None
832
+
833
+ async def list(self, request):
834
+ qs = await self.get_queryset()
835
+ result = await self.paginate_queryset(qs)
836
+
837
+ articles = []
838
+ async for article in result:
839
+ articles.append(ArticleListSchema(
840
+ id=article.id,
841
+ title=article.title,
842
+ author=article.author
843
+ ))
844
+ return articles
845
+
846
+ with TestClient(api) as client:
847
+ response = client.get("/articles")
848
+ assert response.status_code == 200
849
+
850
+ data = response.json()
851
+ assert isinstance(data, list)
852
+ assert len(data) == 50
853
+
854
+
855
+ @pytest.mark.django_db(transaction=True)
856
+ def test_modelviewset_basic_list(sample_articles):
857
+ """Test ModelViewSet basic list operation (pagination can be added manually)"""
858
+ api = BoltAPI()
859
+
860
+ @api.viewset("/articles")
861
+ class ArticleViewSet(ModelViewSet):
862
+ queryset = Article.objects.all()
863
+ serializer_class = ArticleSchema
864
+ list_serializer_class = ArticleListSchema
865
+
866
+ with TestClient(api) as client:
867
+ response = client.get("/articles")
868
+ assert response.status_code == 200
869
+
870
+ data = response.json()
871
+ assert isinstance(data, list)
872
+ assert len(data) == 50 # All articles returned (no pagination)
873
+
874
+
875
+ # ============================================================================
876
+ # Edge Cases and Validation
877
+ # ============================================================================
878
+
879
+ @pytest.mark.django_db(transaction=True)
880
+ def test_pagination_max_page_size_enforcement(sample_articles):
881
+ """Test that max_page_size is enforced"""
882
+ api = BoltAPI()
883
+
884
+ class LimitedPagination(PageNumberPagination):
885
+ page_size = 10
886
+ max_page_size = 25
887
+ page_size_query_param = "page_size"
888
+
889
+ @api.get("/articles")
890
+ @paginate(LimitedPagination)
891
+ async def list_articles(request):
892
+ return Article.objects.all()
893
+
894
+ with TestClient(api) as client:
895
+ response = client.get("/articles?page=1&page_size=100")
896
+ assert response.status_code == 200
897
+
898
+ data = response.json()
899
+ assert data["page_size"] == 25 # Clamped to max
900
+ assert len(data["items"]) == 25
901
+
902
+
903
+ @pytest.mark.django_db(transaction=True)
904
+ def test_pagination_invalid_page_defaults_to_first(sample_articles):
905
+ """Test that invalid page number defaults to page 1"""
906
+ api = BoltAPI()
907
+
908
+ class SmallPagePagination(PageNumberPagination):
909
+ page_size = 10
910
+
911
+ @api.get("/articles")
912
+ @paginate(SmallPagePagination)
913
+ async def list_articles(request):
914
+ return Article.objects.all()
915
+
916
+ with TestClient(api) as client:
917
+ response = client.get("/articles?page=invalid")
918
+ assert response.status_code == 200
919
+
920
+ data = response.json()
921
+ assert data["page"] == 1
922
+
923
+
924
+ @pytest.mark.django_db(transaction=True)
925
+ def test_pagination_page_exceeds_total_clamps_to_last(sample_articles):
926
+ """Test that page number exceeding total pages clamps to last page"""
927
+ api = BoltAPI()
928
+
929
+ class SmallPagePagination(PageNumberPagination):
930
+ page_size = 10
931
+
932
+ @api.get("/articles")
933
+ @paginate(SmallPagePagination)
934
+ async def list_articles(request):
935
+ return Article.objects.all()
936
+
937
+ with TestClient(api) as client:
938
+ response = client.get("/articles?page=999")
939
+ assert response.status_code == 200
940
+
941
+ data = response.json()
942
+ assert data["page"] == 5 # Last page
943
+ assert len(data["items"]) == 10
944
+
945
+
946
+ @pytest.mark.django_db(transaction=True)
947
+ def test_limit_offset_negative_offset_defaults_to_zero(sample_articles):
948
+ """Test that negative offset defaults to 0"""
949
+ api = BoltAPI()
950
+
951
+ @api.get("/articles")
952
+ @paginate(LimitOffsetPagination)
953
+ async def list_articles(request):
954
+ return Article.objects.all()
955
+
956
+ with TestClient(api) as client:
957
+ response = client.get("/articles?limit=10&offset=-5")
958
+ assert response.status_code == 200
959
+
960
+ data = response.json()
961
+ assert data["offset"] == 0
962
+
963
+
964
+ @pytest.mark.django_db(transaction=True)
965
+ def test_pagination_zero_page_size_uses_default(sample_articles):
966
+ """Test that zero page size uses default"""
967
+ api = BoltAPI()
968
+
969
+ class SmallPagePagination(PageNumberPagination):
970
+ page_size = 10
971
+ page_size_query_param = "page_size"
972
+
973
+ @api.get("/articles")
974
+ @paginate(SmallPagePagination)
975
+ async def list_articles(request):
976
+ return Article.objects.all()
977
+
978
+ with TestClient(api) as client:
979
+ response = client.get("/articles?page=1&page_size=0")
980
+ assert response.status_code == 200
981
+
982
+ data = response.json()
983
+ assert data["page_size"] == 10 # Default
984
+
985
+
986
+ @pytest.mark.django_db(transaction=True)
987
+ def test_pagination_with_query_optimization(sample_articles):
988
+ """Test pagination works with .only() query optimization"""
989
+ api = BoltAPI()
990
+
991
+ class SmallPagePagination(PageNumberPagination):
992
+ page_size = 10
993
+
994
+ @api.get("/articles")
995
+ @paginate(SmallPagePagination)
996
+ async def list_articles(request):
997
+ return Article.objects.only("id", "title", "author")
998
+
999
+ with TestClient(api) as client:
1000
+ response = client.get("/articles?page=1")
1001
+ assert response.status_code == 200
1002
+
1003
+ data = response.json()
1004
+ assert len(data["items"]) == 10
1005
+ assert "title" in data["items"][0]
1006
+ assert "author" in data["items"][0]
1007
+
1008
+
1009
+ # ============================================================================
1010
+ # Advanced Integration & Edge Cases
1011
+ # ============================================================================
1012
+
1013
+ @pytest.mark.django_db(transaction=True)
1014
+ def test_pagination_with_select_related(sample_articles):
1015
+ """Test pagination works with select_related"""
1016
+ api = BoltAPI()
1017
+
1018
+ class SmallPagePagination(PageNumberPagination):
1019
+ page_size = 10
1020
+
1021
+ @api.get("/articles")
1022
+ @paginate(SmallPagePagination)
1023
+ async def list_articles(request):
1024
+ # Even though Article doesn't have FK in this test, this tests compatibility
1025
+ return Article.objects.all().select_related()
1026
+
1027
+ with TestClient(api) as client:
1028
+ response = client.get("/articles?page=1")
1029
+ assert response.status_code == 200
1030
+
1031
+ data = response.json()
1032
+ assert len(data["items"]) == 10
1033
+
1034
+
1035
+ @pytest.mark.django_db(transaction=True)
1036
+ def test_pagination_with_prefetch_related(sample_articles):
1037
+ """Test pagination works with prefetch_related"""
1038
+ api = BoltAPI()
1039
+
1040
+ class SmallPagePagination(PageNumberPagination):
1041
+ page_size = 10
1042
+
1043
+ @api.get("/articles")
1044
+ @paginate(SmallPagePagination)
1045
+ async def list_articles(request):
1046
+ # Even though Article doesn't have M2M in this test, this tests compatibility
1047
+ return Article.objects.all().prefetch_related()
1048
+
1049
+ with TestClient(api) as client:
1050
+ response = client.get("/articles?page=1")
1051
+ assert response.status_code == 200
1052
+
1053
+ data = response.json()
1054
+ assert len(data["items"]) == 10
1055
+
1056
+
1057
+ @pytest.mark.django_db(transaction=True)
1058
+ def test_pagination_with_distinct(sample_articles):
1059
+ """Test pagination works with distinct"""
1060
+ api = BoltAPI()
1061
+
1062
+ class SmallPagePagination(PageNumberPagination):
1063
+ page_size = 10
1064
+
1065
+ @api.get("/articles")
1066
+ @paginate(SmallPagePagination)
1067
+ async def list_articles(request):
1068
+ return Article.objects.all().distinct()
1069
+
1070
+ with TestClient(api) as client:
1071
+ response = client.get("/articles?page=1")
1072
+ assert response.status_code == 200
1073
+
1074
+ data = response.json()
1075
+ assert len(data["items"]) == 10
1076
+
1077
+
1078
+ @pytest.mark.django_db(transaction=True)
1079
+ def test_pagination_with_values(sample_articles):
1080
+ """Test pagination works with values()"""
1081
+ api = BoltAPI()
1082
+
1083
+ class SmallPagePagination(PageNumberPagination):
1084
+ page_size = 10
1085
+
1086
+ @api.get("/articles")
1087
+ @paginate(SmallPagePagination)
1088
+ async def list_articles(request):
1089
+ return Article.objects.values("id", "title", "author")
1090
+
1091
+ with TestClient(api) as client:
1092
+ response = client.get("/articles?page=1")
1093
+ assert response.status_code == 200
1094
+
1095
+ data = response.json()
1096
+ assert len(data["items"]) == 10
1097
+ # values() returns dicts
1098
+ assert isinstance(data["items"][0], dict)
1099
+ assert "title" in data["items"][0]
1100
+
1101
+
1102
+ @pytest.mark.django_db(transaction=True)
1103
+ def test_pagination_with_values_list(sample_articles):
1104
+ """Test pagination works with values_list()"""
1105
+ api = BoltAPI()
1106
+
1107
+ class SmallPagePagination(PageNumberPagination):
1108
+ page_size = 10
1109
+
1110
+ @api.get("/articles")
1111
+ @paginate(SmallPagePagination)
1112
+ async def list_articles(request):
1113
+ return Article.objects.values_list("id", "title", "author")
1114
+
1115
+ with TestClient(api) as client:
1116
+ response = client.get("/articles?page=1")
1117
+ assert response.status_code == 200
1118
+
1119
+ data = response.json()
1120
+ assert len(data["items"]) == 10
1121
+ # values_list() returns tuples (serialized as lists)
1122
+ assert isinstance(data["items"][0], list)
1123
+
1124
+
1125
+ @pytest.mark.django_db(transaction=True)
1126
+ def test_pagination_single_result(db):
1127
+ """Test pagination with only one result"""
1128
+ api = BoltAPI()
1129
+
1130
+ # Create single article
1131
+ Article.objects.create(
1132
+ title="Solo Article",
1133
+ content="Content",
1134
+ author="Author",
1135
+ is_published=True,
1136
+ )
1137
+
1138
+ class SmallPagePagination(PageNumberPagination):
1139
+ page_size = 10
1140
+
1141
+ @api.get("/articles")
1142
+ @paginate(SmallPagePagination)
1143
+ async def list_articles(request):
1144
+ return Article.objects.all()
1145
+
1146
+ with TestClient(api) as client:
1147
+ response = client.get("/articles?page=1")
1148
+ assert response.status_code == 200
1149
+
1150
+ data = response.json()
1151
+ assert len(data["items"]) == 1
1152
+ assert data["total"] == 1
1153
+ assert data["total_pages"] == 1
1154
+ assert data["has_next"] is False
1155
+ assert data["has_previous"] is False
1156
+
1157
+
1158
+ @pytest.mark.django_db(transaction=True)
1159
+ def test_limit_offset_with_zero_limit(sample_articles):
1160
+ """Test limit offset with zero limit uses default"""
1161
+ api = BoltAPI()
1162
+
1163
+ class CustomLimitOffset(LimitOffsetPagination):
1164
+ page_size = 20
1165
+
1166
+ @api.get("/articles")
1167
+ @paginate(CustomLimitOffset)
1168
+ async def list_articles(request):
1169
+ return Article.objects.all()
1170
+
1171
+ with TestClient(api) as client:
1172
+ response = client.get("/articles?limit=0&offset=0")
1173
+ assert response.status_code == 200
1174
+
1175
+ data = response.json()
1176
+ assert data["limit"] == 20 # Uses default
1177
+ assert len(data["items"]) == 20
1178
+
1179
+
1180
+ @pytest.mark.django_db(transaction=True)
1181
+ def test_cursor_pagination_invalid_cursor(sample_articles):
1182
+ """Test cursor pagination with invalid cursor value"""
1183
+ api = BoltAPI()
1184
+
1185
+ class SmallCursorPagination(CursorPagination):
1186
+ page_size = 10
1187
+ ordering = "-id"
1188
+
1189
+ @api.get("/articles")
1190
+ @paginate(SmallCursorPagination)
1191
+ async def list_articles(request):
1192
+ return Article.objects.all()
1193
+
1194
+ with TestClient(api) as client:
1195
+ # Use invalid cursor - should start from beginning
1196
+ response = client.get("/articles?cursor=invalid_cursor_value")
1197
+ assert response.status_code == 200
1198
+
1199
+ data = response.json()
1200
+ # Invalid cursor should be ignored and start from beginning
1201
+ assert len(data["items"]) == 10
1202
+
1203
+
1204
+ @pytest.mark.django_db(transaction=True)
1205
+ def test_multiple_pagination_types_different_endpoints(sample_articles):
1206
+ """Test using different pagination types on different endpoints"""
1207
+ api = BoltAPI()
1208
+
1209
+ class PagePagination(PageNumberPagination):
1210
+ page_size = 10
1211
+
1212
+ class OffsetPagination(LimitOffsetPagination):
1213
+ page_size = 15
1214
+
1215
+ class CursorPag(CursorPagination):
1216
+ page_size = 5
1217
+ ordering = "-id"
1218
+
1219
+ @api.get("/articles/page")
1220
+ @paginate(PagePagination)
1221
+ async def list_page(request):
1222
+ return Article.objects.all()
1223
+
1224
+ @api.get("/articles/offset")
1225
+ @paginate(OffsetPagination)
1226
+ async def list_offset(request):
1227
+ return Article.objects.all()
1228
+
1229
+ @api.get("/articles/cursor")
1230
+ @paginate(CursorPag)
1231
+ async def list_cursor(request):
1232
+ return Article.objects.all()
1233
+
1234
+ with TestClient(api) as client:
1235
+ # Test page pagination
1236
+ response1 = client.get("/articles/page?page=1")
1237
+ assert response1.status_code == 200
1238
+ data1 = response1.json()
1239
+ assert len(data1["items"]) == 10
1240
+ assert "page" in data1
1241
+
1242
+ # Test limit-offset pagination (no limit specified, uses page_size default)
1243
+ response2 = client.get("/articles/offset")
1244
+ assert response2.status_code == 200
1245
+ data2 = response2.json()
1246
+ assert len(data2["items"]) == 15
1247
+ assert "limit" in data2
1248
+
1249
+ # Test cursor pagination
1250
+ response3 = client.get("/articles/cursor")
1251
+ assert response3.status_code == 200
1252
+ data3 = response3.json()
1253
+ assert len(data3["items"]) == 5
1254
+ assert "next_cursor" in data3
1255
+
1256
+
1257
+ if __name__ == "__main__":
1258
+ pytest.main([__file__, "-v", "-s"])