Coverage for tests/test_tutorial/test_sql_databases/test_tutorial002.py: 100%
70 statements
« prev ^ index » next coverage.py v7.6.1, created at 2025-01-13 13:38 +0000
« prev ^ index » next coverage.py v7.6.1, created at 2025-01-13 13:38 +0000
1import importlib 1fghij
2import warnings 1fghij
4import pytest 1fghij
5from dirty_equals import IsDict, IsInt 1fghij
6from fastapi.testclient import TestClient 1fghij
7from inline_snapshot import snapshot 1fghij
8from sqlalchemy import StaticPool 1fghij
9from sqlmodel import SQLModel, create_engine 1fghij
10from sqlmodel.main import default_registry 1fghij
12from tests.utils import needs_py39, needs_py310 1fghij
15def clear_sqlmodel(): 1fghij
16 # Clear the tables in the metadata for the default base model
17 SQLModel.metadata.clear() 1fghij
18 # Clear the Models associated with the registry, to avoid warnings
19 default_registry.dispose() 1fghij
22@pytest.fixture( 1fghij
23 name="client",
24 params=[
25 "tutorial002",
26 pytest.param("tutorial002_py39", marks=needs_py39),
27 pytest.param("tutorial002_py310", marks=needs_py310),
28 "tutorial002_an",
29 pytest.param("tutorial002_an_py39", marks=needs_py39),
30 pytest.param("tutorial002_an_py310", marks=needs_py310),
31 ],
32)
33def get_client(request: pytest.FixtureRequest): 1fghij
34 clear_sqlmodel() 1fghij
35 # TODO: remove when updating SQL tutorial to use new lifespan API
36 with warnings.catch_warnings(record=True): 1fghij
37 warnings.simplefilter("always") 1fghij
38 mod = importlib.import_module(f"docs_src.sql_databases.{request.param}") 1fghij
39 clear_sqlmodel() 1fghij
40 importlib.reload(mod) 1fghij
41 mod.sqlite_url = "sqlite://" 1fghij
42 mod.engine = create_engine( 1fghij
43 mod.sqlite_url, connect_args={"check_same_thread": False}, poolclass=StaticPool
44 )
46 with TestClient(mod.app) as c: 1fghij
47 yield c 1fghij
50def test_crud_app(client: TestClient): 1fghij
51 # TODO: this warns that SQLModel.from_orm is deprecated in Pydantic v1, refactor
52 # this if using obj.model_validate becomes independent of Pydantic v2
53 with warnings.catch_warnings(record=True): 1abcde
54 warnings.simplefilter("always") 1abcde
55 # No heroes before creating
56 response = client.get("heroes/") 1abcde
57 assert response.status_code == 200, response.text 1abcde
58 assert response.json() == [] 1abcde
60 # Create a hero
61 response = client.post( 1abcde
62 "/heroes/",
63 json={
64 "id": 9000,
65 "name": "Dead Pond",
66 "age": 30,
67 "secret_name": "Dive Wilson",
68 },
69 )
70 assert response.status_code == 200, response.text 1abcde
71 assert response.json() == snapshot( 1abcde
72 {"age": 30, "id": IsInt(), "name": "Dead Pond"}
73 )
74 assert ( 1abcde
75 response.json()["id"] != 9000
76 ), "The ID should be generated by the database"
78 # Read a hero
79 hero_id = response.json()["id"] 1abcde
80 response = client.get(f"/heroes/{hero_id}") 1abcde
81 assert response.status_code == 200, response.text 1abcde
82 assert response.json() == snapshot( 1abcde
83 {"name": "Dead Pond", "age": 30, "id": IsInt()}
84 )
86 # Read all heroes
87 # Create more heroes first
88 response = client.post( 1abcde
89 "/heroes/",
90 json={"name": "Spider-Boy", "age": 18, "secret_name": "Pedro Parqueador"},
91 )
92 assert response.status_code == 200, response.text 1abcde
93 response = client.post( 1abcde
94 "/heroes/", json={"name": "Rusty-Man", "secret_name": "Tommy Sharp"}
95 )
96 assert response.status_code == 200, response.text 1abcde
98 response = client.get("/heroes/") 1abcde
99 assert response.status_code == 200, response.text 1abcde
100 assert response.json() == snapshot( 1abcde
101 [
102 {"name": "Dead Pond", "age": 30, "id": IsInt()},
103 {"name": "Spider-Boy", "age": 18, "id": IsInt()},
104 {"name": "Rusty-Man", "age": None, "id": IsInt()},
105 ]
106 )
108 response = client.get("/heroes/?offset=1&limit=1") 1abcde
109 assert response.status_code == 200, response.text 1abcde
110 assert response.json() == snapshot( 1abcde
111 [{"name": "Spider-Boy", "age": 18, "id": IsInt()}]
112 )
114 # Update a hero
115 response = client.patch( 1abcde
116 f"/heroes/{hero_id}", json={"name": "Dog Pond", "age": None}
117 )
118 assert response.status_code == 200, response.text 1abcde
119 assert response.json() == snapshot( 1abcde
120 {"name": "Dog Pond", "age": None, "id": hero_id}
121 )
123 # Get updated hero
124 response = client.get(f"/heroes/{hero_id}") 1abcde
125 assert response.status_code == 200, response.text 1abcde
126 assert response.json() == snapshot( 1abcde
127 {"name": "Dog Pond", "age": None, "id": hero_id}
128 )
130 # Delete a hero
131 response = client.delete(f"/heroes/{hero_id}") 1abcde
132 assert response.status_code == 200, response.text 1abcde
133 assert response.json() == snapshot({"ok": True}) 1abcde
135 # The hero is no longer found
136 response = client.get(f"/heroes/{hero_id}") 1abcde
137 assert response.status_code == 404, response.text 1abcde
139 # Delete a hero that does not exist
140 response = client.delete(f"/heroes/{hero_id}") 1abcde
141 assert response.status_code == 404, response.text 1abcde
142 assert response.json() == snapshot({"detail": "Hero not found"}) 1abcde
144 # Update a hero that does not exist
145 response = client.patch(f"/heroes/{hero_id}", json={"name": "Dog Pond"}) 1abcde
146 assert response.status_code == 404, response.text 1abcde
147 assert response.json() == snapshot({"detail": "Hero not found"}) 1abcde
150def test_openapi_schema(client: TestClient): 1fghij
151 response = client.get("/openapi.json") 1klmno
152 assert response.status_code == 200, response.text 1klmno
153 assert response.json() == snapshot( 1klmno
154 {
155 "openapi": "3.1.0",
156 "info": {"title": "FastAPI", "version": "0.1.0"},
157 "paths": {
158 "/heroes/": {
159 "post": {
160 "summary": "Create Hero",
161 "operationId": "create_hero_heroes__post",
162 "requestBody": {
163 "required": True,
164 "content": {
165 "application/json": {
166 "schema": {
167 "$ref": "#/components/schemas/HeroCreate"
168 }
169 }
170 },
171 },
172 "responses": {
173 "200": {
174 "description": "Successful Response",
175 "content": {
176 "application/json": {
177 "schema": {
178 "$ref": "#/components/schemas/HeroPublic"
179 }
180 }
181 },
182 },
183 "422": {
184 "description": "Validation Error",
185 "content": {
186 "application/json": {
187 "schema": {
188 "$ref": "#/components/schemas/HTTPValidationError"
189 }
190 }
191 },
192 },
193 },
194 },
195 "get": {
196 "summary": "Read Heroes",
197 "operationId": "read_heroes_heroes__get",
198 "parameters": [
199 {
200 "name": "offset",
201 "in": "query",
202 "required": False,
203 "schema": {
204 "type": "integer",
205 "default": 0,
206 "title": "Offset",
207 },
208 },
209 {
210 "name": "limit",
211 "in": "query",
212 "required": False,
213 "schema": {
214 "type": "integer",
215 "maximum": 100,
216 "default": 100,
217 "title": "Limit",
218 },
219 },
220 ],
221 "responses": {
222 "200": {
223 "description": "Successful Response",
224 "content": {
225 "application/json": {
226 "schema": {
227 "type": "array",
228 "items": {
229 "$ref": "#/components/schemas/HeroPublic"
230 },
231 "title": "Response Read Heroes Heroes Get",
232 }
233 }
234 },
235 },
236 "422": {
237 "description": "Validation Error",
238 "content": {
239 "application/json": {
240 "schema": {
241 "$ref": "#/components/schemas/HTTPValidationError"
242 }
243 }
244 },
245 },
246 },
247 },
248 },
249 "/heroes/{hero_id}": {
250 "get": {
251 "summary": "Read Hero",
252 "operationId": "read_hero_heroes__hero_id__get",
253 "parameters": [
254 {
255 "name": "hero_id",
256 "in": "path",
257 "required": True,
258 "schema": {"type": "integer", "title": "Hero Id"},
259 }
260 ],
261 "responses": {
262 "200": {
263 "description": "Successful Response",
264 "content": {
265 "application/json": {
266 "schema": {
267 "$ref": "#/components/schemas/HeroPublic"
268 }
269 }
270 },
271 },
272 "422": {
273 "description": "Validation Error",
274 "content": {
275 "application/json": {
276 "schema": {
277 "$ref": "#/components/schemas/HTTPValidationError"
278 }
279 }
280 },
281 },
282 },
283 },
284 "patch": {
285 "summary": "Update Hero",
286 "operationId": "update_hero_heroes__hero_id__patch",
287 "parameters": [
288 {
289 "name": "hero_id",
290 "in": "path",
291 "required": True,
292 "schema": {"type": "integer", "title": "Hero Id"},
293 }
294 ],
295 "requestBody": {
296 "required": True,
297 "content": {
298 "application/json": {
299 "schema": {
300 "$ref": "#/components/schemas/HeroUpdate"
301 }
302 }
303 },
304 },
305 "responses": {
306 "200": {
307 "description": "Successful Response",
308 "content": {
309 "application/json": {
310 "schema": {
311 "$ref": "#/components/schemas/HeroPublic"
312 }
313 }
314 },
315 },
316 "422": {
317 "description": "Validation Error",
318 "content": {
319 "application/json": {
320 "schema": {
321 "$ref": "#/components/schemas/HTTPValidationError"
322 }
323 }
324 },
325 },
326 },
327 },
328 "delete": {
329 "summary": "Delete Hero",
330 "operationId": "delete_hero_heroes__hero_id__delete",
331 "parameters": [
332 {
333 "name": "hero_id",
334 "in": "path",
335 "required": True,
336 "schema": {"type": "integer", "title": "Hero Id"},
337 }
338 ],
339 "responses": {
340 "200": {
341 "description": "Successful Response",
342 "content": {"application/json": {"schema": {}}},
343 },
344 "422": {
345 "description": "Validation Error",
346 "content": {
347 "application/json": {
348 "schema": {
349 "$ref": "#/components/schemas/HTTPValidationError"
350 }
351 }
352 },
353 },
354 },
355 },
356 },
357 },
358 "components": {
359 "schemas": {
360 "HTTPValidationError": {
361 "properties": {
362 "detail": {
363 "items": {
364 "$ref": "#/components/schemas/ValidationError"
365 },
366 "type": "array",
367 "title": "Detail",
368 }
369 },
370 "type": "object",
371 "title": "HTTPValidationError",
372 },
373 "HeroCreate": {
374 "properties": {
375 "name": {"type": "string", "title": "Name"},
376 "age": IsDict(
377 {
378 "anyOf": [{"type": "integer"}, {"type": "null"}],
379 "title": "Age",
380 }
381 )
382 | IsDict(
383 # TODO: remove when deprecating Pydantic v1
384 {
385 "type": "integer",
386 "title": "Age",
387 }
388 ),
389 "secret_name": {"type": "string", "title": "Secret Name"},
390 },
391 "type": "object",
392 "required": ["name", "secret_name"],
393 "title": "HeroCreate",
394 },
395 "HeroPublic": {
396 "properties": {
397 "name": {"type": "string", "title": "Name"},
398 "age": IsDict(
399 {
400 "anyOf": [{"type": "integer"}, {"type": "null"}],
401 "title": "Age",
402 }
403 )
404 | IsDict(
405 # TODO: remove when deprecating Pydantic v1
406 {
407 "type": "integer",
408 "title": "Age",
409 }
410 ),
411 "id": {"type": "integer", "title": "Id"},
412 },
413 "type": "object",
414 "required": ["name", "id"],
415 "title": "HeroPublic",
416 },
417 "HeroUpdate": {
418 "properties": {
419 "name": IsDict(
420 {
421 "anyOf": [{"type": "string"}, {"type": "null"}],
422 "title": "Name",
423 }
424 )
425 | IsDict(
426 # TODO: remove when deprecating Pydantic v1
427 {
428 "type": "string",
429 "title": "Name",
430 }
431 ),
432 "age": IsDict(
433 {
434 "anyOf": [{"type": "integer"}, {"type": "null"}],
435 "title": "Age",
436 }
437 )
438 | IsDict(
439 # TODO: remove when deprecating Pydantic v1
440 {
441 "type": "integer",
442 "title": "Age",
443 }
444 ),
445 "secret_name": IsDict(
446 {
447 "anyOf": [{"type": "string"}, {"type": "null"}],
448 "title": "Secret Name",
449 }
450 )
451 | IsDict(
452 # TODO: remove when deprecating Pydantic v1
453 {
454 "type": "string",
455 "title": "Secret Name",
456 }
457 ),
458 },
459 "type": "object",
460 "title": "HeroUpdate",
461 },
462 "ValidationError": {
463 "properties": {
464 "loc": {
465 "items": {
466 "anyOf": [{"type": "string"}, {"type": "integer"}]
467 },
468 "type": "array",
469 "title": "Location",
470 },
471 "msg": {"type": "string", "title": "Message"},
472 "type": {"type": "string", "title": "Error Type"},
473 },
474 "type": "object",
475 "required": ["loc", "msg", "type"],
476 "title": "ValidationError",
477 },
478 }
479 },
480 }
481 )