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

1import importlib 1hijklmn

2import warnings 1hijklmn

3 

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

11 

12from tests.utils import needs_py39, needs_py310 1hijklmn

13 

14 

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

20 

21 

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 ) 

45 

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

50 

51 

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

61 

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 ) 

79 

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 ) 

87 

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

99 

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 ) 

109 

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 ) 

115 

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 ) 

124 

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 ) 

131 

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

136 

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

140 

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

145 

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

150 

151 

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 )