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