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