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

1import importlib 1degf

2import warnings 1degf

3 

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

11 

12from tests.utils import needs_py310 1degf

13 

14 

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

20 

21 

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 ) 

41 

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

46 

47 

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

57 

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 ) 

75 

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 ) 

83 

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

95 

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 ) 

105 

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 ) 

111 

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 ) 

120 

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 ) 

127 

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

132 

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

136 

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

141 

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

146 

147 

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 )