Coverage for tests/test_tutorial/test_sql_databases/test_tutorial001.py: 100%
61 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 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 "tutorial001",
26 pytest.param("tutorial001_py39", marks=needs_py39),
27 pytest.param("tutorial001_py310", marks=needs_py310),
28 "tutorial001_an",
29 pytest.param("tutorial001_an_py39", marks=needs_py39),
30 pytest.param("tutorial001_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": 999,
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, "secret_name": "Dive Wilson", "id": 999, "name": "Dead Pond"}
75 )
77 # Read a hero
78 hero_id = response.json()["id"] 1abcdefg
79 response = client.get(f"/heroes/{hero_id}") 1abcdefg
80 assert response.status_code == 200, response.text 1abcdefg
81 assert response.json() == snapshot( 1abcdefg
82 {"name": "Dead Pond", "age": 30, "id": 999, "secret_name": "Dive Wilson"}
83 )
85 # Read all heroes
86 # Create more heroes first
87 response = client.post( 1abcdefg
88 "/heroes/",
89 json={"name": "Spider-Boy", "age": 18, "secret_name": "Pedro Parqueador"},
90 )
91 assert response.status_code == 200, response.text 1abcdefg
92 response = client.post( 1abcdefg
93 "/heroes/", json={"name": "Rusty-Man", "secret_name": "Tommy Sharp"}
94 )
95 assert response.status_code == 200, response.text 1abcdefg
97 response = client.get("/heroes/") 1abcdefg
98 assert response.status_code == 200, response.text 1abcdefg
99 assert response.json() == snapshot( 1abcdefg
100 [
101 {
102 "name": "Dead Pond",
103 "age": 30,
104 "id": IsInt(),
105 "secret_name": "Dive Wilson",
106 },
107 {
108 "name": "Spider-Boy",
109 "age": 18,
110 "id": IsInt(),
111 "secret_name": "Pedro Parqueador",
112 },
113 {
114 "name": "Rusty-Man",
115 "age": None,
116 "id": IsInt(),
117 "secret_name": "Tommy Sharp",
118 },
119 ]
120 )
122 response = client.get("/heroes/?offset=1&limit=1") 1abcdefg
123 assert response.status_code == 200, response.text 1abcdefg
124 assert response.json() == snapshot( 1abcdefg
125 [
126 {
127 "name": "Spider-Boy",
128 "age": 18,
129 "id": IsInt(),
130 "secret_name": "Pedro Parqueador",
131 }
132 ]
133 )
135 # Delete a hero
136 response = client.delete(f"/heroes/{hero_id}") 1abcdefg
137 assert response.status_code == 200, response.text 1abcdefg
138 assert response.json() == snapshot({"ok": True}) 1abcdefg
140 response = client.get(f"/heroes/{hero_id}") 1abcdefg
141 assert response.status_code == 404, response.text 1abcdefg
143 response = client.delete(f"/heroes/{hero_id}") 1abcdefg
144 assert response.status_code == 404, response.text 1abcdefg
145 assert response.json() == snapshot({"detail": "Hero not found"}) 1abcdefg
148def test_openapi_schema(client: TestClient): 1hijklmn
149 response = client.get("/openapi.json") 1opqrstu
150 assert response.status_code == 200, response.text 1opqrstu
151 assert response.json() == snapshot( 1opqrstu
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": {"$ref": "#/components/schemas/Hero"}
165 }
166 },
167 },
168 "responses": {
169 "200": {
170 "description": "Successful Response",
171 "content": {
172 "application/json": {
173 "schema": {"$ref": "#/components/schemas/Hero"}
174 }
175 },
176 },
177 "422": {
178 "description": "Validation Error",
179 "content": {
180 "application/json": {
181 "schema": {
182 "$ref": "#/components/schemas/HTTPValidationError"
183 }
184 }
185 },
186 },
187 },
188 },
189 "get": {
190 "summary": "Read Heroes",
191 "operationId": "read_heroes_heroes__get",
192 "parameters": [
193 {
194 "name": "offset",
195 "in": "query",
196 "required": False,
197 "schema": {
198 "type": "integer",
199 "default": 0,
200 "title": "Offset",
201 },
202 },
203 {
204 "name": "limit",
205 "in": "query",
206 "required": False,
207 "schema": {
208 "type": "integer",
209 "maximum": 100,
210 "default": 100,
211 "title": "Limit",
212 },
213 },
214 ],
215 "responses": {
216 "200": {
217 "description": "Successful Response",
218 "content": {
219 "application/json": {
220 "schema": {
221 "type": "array",
222 "items": {
223 "$ref": "#/components/schemas/Hero"
224 },
225 "title": "Response Read Heroes Heroes Get",
226 }
227 }
228 },
229 },
230 "422": {
231 "description": "Validation Error",
232 "content": {
233 "application/json": {
234 "schema": {
235 "$ref": "#/components/schemas/HTTPValidationError"
236 }
237 }
238 },
239 },
240 },
241 },
242 },
243 "/heroes/{hero_id}": {
244 "get": {
245 "summary": "Read Hero",
246 "operationId": "read_hero_heroes__hero_id__get",
247 "parameters": [
248 {
249 "name": "hero_id",
250 "in": "path",
251 "required": True,
252 "schema": {"type": "integer", "title": "Hero Id"},
253 }
254 ],
255 "responses": {
256 "200": {
257 "description": "Successful Response",
258 "content": {
259 "application/json": {
260 "schema": {"$ref": "#/components/schemas/Hero"}
261 }
262 },
263 },
264 "422": {
265 "description": "Validation Error",
266 "content": {
267 "application/json": {
268 "schema": {
269 "$ref": "#/components/schemas/HTTPValidationError"
270 }
271 }
272 },
273 },
274 },
275 },
276 "delete": {
277 "summary": "Delete Hero",
278 "operationId": "delete_hero_heroes__hero_id__delete",
279 "parameters": [
280 {
281 "name": "hero_id",
282 "in": "path",
283 "required": True,
284 "schema": {"type": "integer", "title": "Hero Id"},
285 }
286 ],
287 "responses": {
288 "200": {
289 "description": "Successful Response",
290 "content": {"application/json": {"schema": {}}},
291 },
292 "422": {
293 "description": "Validation Error",
294 "content": {
295 "application/json": {
296 "schema": {
297 "$ref": "#/components/schemas/HTTPValidationError"
298 }
299 }
300 },
301 },
302 },
303 },
304 },
305 },
306 "components": {
307 "schemas": {
308 "HTTPValidationError": {
309 "properties": {
310 "detail": {
311 "items": {
312 "$ref": "#/components/schemas/ValidationError"
313 },
314 "type": "array",
315 "title": "Detail",
316 }
317 },
318 "type": "object",
319 "title": "HTTPValidationError",
320 },
321 "Hero": {
322 "properties": {
323 "id": IsDict(
324 {
325 "anyOf": [{"type": "integer"}, {"type": "null"}],
326 "title": "Id",
327 }
328 )
329 | IsDict(
330 # TODO: remove when deprecating Pydantic v1
331 {
332 "type": "integer",
333 "title": "Id",
334 }
335 ),
336 "name": {"type": "string", "title": "Name"},
337 "age": IsDict(
338 {
339 "anyOf": [{"type": "integer"}, {"type": "null"}],
340 "title": "Age",
341 }
342 )
343 | IsDict(
344 # TODO: remove when deprecating Pydantic v1
345 {
346 "type": "integer",
347 "title": "Age",
348 }
349 ),
350 "secret_name": {"type": "string", "title": "Secret Name"},
351 },
352 "type": "object",
353 "required": ["name", "secret_name"],
354 "title": "Hero",
355 },
356 "ValidationError": {
357 "properties": {
358 "loc": {
359 "items": {
360 "anyOf": [{"type": "string"}, {"type": "integer"}]
361 },
362 "type": "array",
363 "title": "Location",
364 },
365 "msg": {"type": "string", "title": "Message"},
366 "type": {"type": "string", "title": "Error Type"},
367 },
368 "type": "object",
369 "required": ["loc", "msg", "type"],
370 "title": "ValidationError",
371 },
372 }
373 },
374 }
375 )