Coverage for test\test_pagination.py: 94%
160 statements
« prev ^ index » next coverage.py v7.8.0, created at 2025-05-21 20:31 +0100
« prev ^ index » next coverage.py v7.8.0, created at 2025-05-21 20:31 +0100
1import pytest
2from nexios import get_application, NexiosApp
3from nexios.http import Request, Response
4from nexios.testing import Client
5from nexios.pagination import (
6 AsyncPaginator,
7 PageNumberPagination,
8 LimitOffsetPagination,
9 CursorPagination,
10 AsyncListDataHandler as ListDataHandler,
11 PaginationError,
12 InvalidPageError,
13 InvalidPageSizeError,
14 InvalidCursorError,
15)
18@pytest.fixture
19async def test_client():
20 app = get_application()
21 async with Client(app) as client:
22 yield client, app
25# Sample test data
26TEST_DATA = [{"id": i, "name": f"Item {i}"} for i in range(1, 101)]
29async def test_page_number_pagination_in_app(test_client):
30 client, app = test_client
32 @app.get("/items")
33 async def get_items(req: Request, res: Response):
34 handler = ListDataHandler(TEST_DATA)
35 pagination = PageNumberPagination()
36 base_url = str(req.url)
38 try:
39 paginator = AsyncPaginator(
40 handler, pagination, base_url, dict(req.query_params)
41 )
42 result = await paginator.paginate()
43 return res.json(result)
44 except PaginationError as e:
45 return res.json({"error": str(e)}, status_code=400)
47 # Test basic pagination
48 response = await client.get("/items?page=2&page_size=10")
49 data = response.json()
50 assert response.status_code == 200
51 assert len(data["items"]) == 10
52 assert data["items"][0]["id"] == 11
53 assert data["pagination"]["page"] == 2
54 assert data["pagination"]["total_items"] == 100
55 assert "next" in data["pagination"]["links"]
56 assert "prev" in data["pagination"]["links"]
58 # Test invalid page
59 response = await client.get("/items?page=0")
60 assert response.status_code == 400
61 assert "Page number must be at least 1" in response.json()["error"]
63 # Test max page size
64 response = await client.get("/items?page_size=200")
65 data = response.json()
66 assert data["pagination"]["page_size"] == 100 # Max page size enforced
69async def test_limit_offset_pagination_in_app(test_client):
70 client, app = test_client
72 @app.get("/items-limit-offset")
73 async def get_items(req: Request, res: Response):
74 handler = ListDataHandler(TEST_DATA)
75 pagination = LimitOffsetPagination()
76 base_url = str(req.url)
78 try:
79 paginator = AsyncPaginator(
80 handler, pagination, base_url, dict(req.query_params)
81 )
82 result = await paginator.paginate()
83 return res.json(result)
84 except PaginationError as e:
85 return res.json({"error": str(e)}, status_code=400)
87 # Test basic pagination
88 response = await client.get("/items-limit-offset?limit=15&offset=30")
89 data = response.json()
90 assert response.status_code == 200
91 assert len(data["items"]) == 15
92 assert data["items"][0]["id"] == 31
93 assert data["pagination"]["offset"] == 30
94 assert data["pagination"]["total_items"] == 100
95 assert "next" in data["pagination"]["links"]
96 assert "prev" in data["pagination"]["links"]
98 # Test invalid offset
99 response = await client.get("/items-limit-offset?offset=-1")
100 assert response.status_code == 400
101 assert "Offset cannot be negative" in response.json()["error"]
104async def test_cursor_pagination_in_app(test_client):
105 client, app = test_client
107 @app.get("/items-cursor")
108 async def get_items(req: Request, res: Response):
109 handler = ListDataHandler(TEST_DATA)
110 pagination = CursorPagination()
111 base_url = str(req.url)
113 try:
114 paginator = AsyncPaginator(
115 handler, pagination, base_url, dict(req.query_params)
116 )
117 result = await paginator.paginate()
118 return res.json(result)
119 except PaginationError as e:
120 return res.json({"error": str(e)}, status_code=400)
122 # Test initial request
123 response = await client.get("/items-cursor?page_size=10")
124 data = response.json()
125 assert response.status_code == 200
126 assert len(data["items"]) == 10
127 assert "next" in data["pagination"]["links"]
128 assert "prev" not in data["pagination"]["links"] # No prev on first page
130 # Test with cursor
131 next_cursor = data["pagination"]["links"]["next"].split("cursor=")[1].split("&")[0]
132 response = await client.get(f"/items-cursor?cursor={next_cursor}&page_size=10")
133 data = response.json()
134 assert response.status_code == 200
135 assert len(data["items"]) == 10
136 # assert data["items"][0]["id"] == 11
137 assert "next" in data["pagination"]["links"]
138 assert "prev" in data["pagination"]["links"]
140 # Test invalid cursor
141 response = await client.get("/items-cursor?cursor=invalid")
142 # assert response.status_code == 400
143 # assert "Invalid cursor format" in response.json()["error"]
146async def test_pagination_with_filters(test_client):
147 client, app = test_client
149 # Create filtered data handler
150 class FilteredDataHandler(ListDataHandler):
151 async def get_total_items(self) -> int:
152 return len([item for item in self.data if item["id"] % 2 == 0])
154 async def get_items(self, offset: int, limit: int) -> list:
155 filtered = [item for item in self.data if item["id"] % 2 == 0]
156 return filtered[offset : offset + limit]
158 @app.get("/filtered-items")
159 async def get_filtered_items(req: Request, res: Response):
160 handler = FilteredDataHandler(TEST_DATA)
161 pagination = PageNumberPagination()
162 base_url = str(req.url)
164 try:
165 paginator = AsyncPaginator(
166 handler, pagination, base_url, dict(req.query_params)
167 )
168 result = await paginator.paginate()
169 return res.json(result)
170 except PaginationError as e:
171 return res.json({"error": str(e)}, status_code=400)
173 response = await client.get("/filtered-items?page=2&page_size=10")
174 data = response.json()
175 assert response.status_code == 200
176 assert len(data["items"]) == 10
177 assert all(item["id"] % 2 == 0 for item in data["items"])
178 assert data["pagination"]["total_items"] == 50 # Only even IDs
181async def test_pagination_error_handling(test_client):
182 client, app = test_client
184 @app.get("/error-test")
185 async def error_test(req: Request, res: Response):
186 handler = ListDataHandler([])
187 pagination = PageNumberPagination()
188 base_url = str(req.url)
190 try:
191 paginator = AsyncPaginator(
192 handler,
193 pagination,
194 base_url,
195 dict(req.query_params),
196 validate_total_items=False,
197 )
198 result = await paginator.paginate()
199 return res.json(result)
200 except InvalidPageError as e:
201 return res.json({"error": str(e)}, status_code=400)
202 except InvalidPageSizeError as e:
203 return res.json({"error": str(e)}, status_code=400)
204 except InvalidCursorError as e:
205 return res.json({"error": str(e)}, status_code=400)
206 except PaginationError as e:
207 return res.json({"error": str(e)}, status_code=500)
209 # Test various error cases
210 response = await client.get("/error-test?page=0")
211 assert response.status_code == 400
212 assert "Page number must be at least 1" in response.json()["error"]
214 response = await client.get("/error-test?page_size=0")
215 assert response.status_code == 400
216 assert "Page size must be at least 1" in response.json()["error"]
219async def test_pagination_with_custom_metadata(test_client):
220 client, app = test_client
222 class CustomPagination(PageNumberPagination):
223 def generate_metadata(self, total_items, items, base_url, request_params):
224 metadata = super().generate_metadata(
225 total_items, items, base_url, request_params
226 )
227 metadata["custom_field"] = "custom_value"
228 return metadata
230 @app.get("/custom-metadata")
231 async def custom_metadata(req: Request, res: Response):
232 handler = ListDataHandler(TEST_DATA)
233 pagination = CustomPagination()
234 base_url = str(req.url)
236 paginator = AsyncPaginator(
237 handler, pagination, base_url, dict(req.query_params)
238 )
239 result = await paginator.paginate()
240 return res.json(result)
242 response = await client.get("/custom-metadata?page=1")
243 data = response.json()
244 assert response.status_code == 200
245 assert data["pagination"]["custom_field"] == "custom_value"