Coverage for tests/test_tutorial/test_sql_databases/test_tutorial002.py: 100%

70 statements  

« prev     ^ index     » next       coverage.py v7.6.1, created at 2025-01-13 13:38 +0000

1import importlib 1fghij

2import warnings 1fghij

3 

4import pytest 1fghij

5from dirty_equals import IsDict, IsInt 1fghij

6from fastapi.testclient import TestClient 1fghij

7from inline_snapshot import snapshot 1fghij

8from sqlalchemy import StaticPool 1fghij

9from sqlmodel import SQLModel, create_engine 1fghij

10from sqlmodel.main import default_registry 1fghij

11 

12from tests.utils import needs_py39, needs_py310 1fghij

13 

14 

15def clear_sqlmodel(): 1fghij

16 # Clear the tables in the metadata for the default base model 

17 SQLModel.metadata.clear() 1fghij

18 # Clear the Models associated with the registry, to avoid warnings 

19 default_registry.dispose() 1fghij

20 

21 

22@pytest.fixture( 1fghij

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): 1fghij

34 clear_sqlmodel() 1fghij

35 # TODO: remove when updating SQL tutorial to use new lifespan API 

36 with warnings.catch_warnings(record=True): 1fghij

37 warnings.simplefilter("always") 1fghij

38 mod = importlib.import_module(f"docs_src.sql_databases.{request.param}") 1fghij

39 clear_sqlmodel() 1fghij

40 importlib.reload(mod) 1fghij

41 mod.sqlite_url = "sqlite://" 1fghij

42 mod.engine = create_engine( 1fghij

43 mod.sqlite_url, connect_args={"check_same_thread": False}, poolclass=StaticPool 

44 ) 

45 

46 with TestClient(mod.app) as c: 1fghij

47 yield c 1fghij

48 

49 

50def test_crud_app(client: TestClient): 1fghij

51 # TODO: this warns that SQLModel.from_orm is deprecated in Pydantic v1, refactor 

52 # this if using obj.model_validate becomes independent of Pydantic v2 

53 with warnings.catch_warnings(record=True): 1abcde

54 warnings.simplefilter("always") 1abcde

55 # No heroes before creating 

56 response = client.get("heroes/") 1abcde

57 assert response.status_code == 200, response.text 1abcde

58 assert response.json() == [] 1abcde

59 

60 # Create a hero 

61 response = client.post( 1abcde

62 "/heroes/", 

63 json={ 

64 "id": 9000, 

65 "name": "Dead Pond", 

66 "age": 30, 

67 "secret_name": "Dive Wilson", 

68 }, 

69 ) 

70 assert response.status_code == 200, response.text 1abcde

71 assert response.json() == snapshot( 1abcde

72 {"age": 30, "id": IsInt(), "name": "Dead Pond"} 

73 ) 

74 assert ( 1abcde

75 response.json()["id"] != 9000 

76 ), "The ID should be generated by the database" 

77 

78 # Read a hero 

79 hero_id = response.json()["id"] 1abcde

80 response = client.get(f"/heroes/{hero_id}") 1abcde

81 assert response.status_code == 200, response.text 1abcde

82 assert response.json() == snapshot( 1abcde

83 {"name": "Dead Pond", "age": 30, "id": IsInt()} 

84 ) 

85 

86 # Read all heroes 

87 # Create more heroes first 

88 response = client.post( 1abcde

89 "/heroes/", 

90 json={"name": "Spider-Boy", "age": 18, "secret_name": "Pedro Parqueador"}, 

91 ) 

92 assert response.status_code == 200, response.text 1abcde

93 response = client.post( 1abcde

94 "/heroes/", json={"name": "Rusty-Man", "secret_name": "Tommy Sharp"} 

95 ) 

96 assert response.status_code == 200, response.text 1abcde

97 

98 response = client.get("/heroes/") 1abcde

99 assert response.status_code == 200, response.text 1abcde

100 assert response.json() == snapshot( 1abcde

101 [ 

102 {"name": "Dead Pond", "age": 30, "id": IsInt()}, 

103 {"name": "Spider-Boy", "age": 18, "id": IsInt()}, 

104 {"name": "Rusty-Man", "age": None, "id": IsInt()}, 

105 ] 

106 ) 

107 

108 response = client.get("/heroes/?offset=1&limit=1") 1abcde

109 assert response.status_code == 200, response.text 1abcde

110 assert response.json() == snapshot( 1abcde

111 [{"name": "Spider-Boy", "age": 18, "id": IsInt()}] 

112 ) 

113 

114 # Update a hero 

115 response = client.patch( 1abcde

116 f"/heroes/{hero_id}", json={"name": "Dog Pond", "age": None} 

117 ) 

118 assert response.status_code == 200, response.text 1abcde

119 assert response.json() == snapshot( 1abcde

120 {"name": "Dog Pond", "age": None, "id": hero_id} 

121 ) 

122 

123 # Get updated hero 

124 response = client.get(f"/heroes/{hero_id}") 1abcde

125 assert response.status_code == 200, response.text 1abcde

126 assert response.json() == snapshot( 1abcde

127 {"name": "Dog Pond", "age": None, "id": hero_id} 

128 ) 

129 

130 # Delete a hero 

131 response = client.delete(f"/heroes/{hero_id}") 1abcde

132 assert response.status_code == 200, response.text 1abcde

133 assert response.json() == snapshot({"ok": True}) 1abcde

134 

135 # The hero is no longer found 

136 response = client.get(f"/heroes/{hero_id}") 1abcde

137 assert response.status_code == 404, response.text 1abcde

138 

139 # Delete a hero that does not exist 

140 response = client.delete(f"/heroes/{hero_id}") 1abcde

141 assert response.status_code == 404, response.text 1abcde

142 assert response.json() == snapshot({"detail": "Hero not found"}) 1abcde

143 

144 # Update a hero that does not exist 

145 response = client.patch(f"/heroes/{hero_id}", json={"name": "Dog Pond"}) 1abcde

146 assert response.status_code == 404, response.text 1abcde

147 assert response.json() == snapshot({"detail": "Hero not found"}) 1abcde

148 

149 

150def test_openapi_schema(client: TestClient): 1fghij

151 response = client.get("/openapi.json") 1klmno

152 assert response.status_code == 200, response.text 1klmno

153 assert response.json() == snapshot( 1klmno

154 { 

155 "openapi": "3.1.0", 

156 "info": {"title": "FastAPI", "version": "0.1.0"}, 

157 "paths": { 

158 "/heroes/": { 

159 "post": { 

160 "summary": "Create Hero", 

161 "operationId": "create_hero_heroes__post", 

162 "requestBody": { 

163 "required": True, 

164 "content": { 

165 "application/json": { 

166 "schema": { 

167 "$ref": "#/components/schemas/HeroCreate" 

168 } 

169 } 

170 }, 

171 }, 

172 "responses": { 

173 "200": { 

174 "description": "Successful Response", 

175 "content": { 

176 "application/json": { 

177 "schema": { 

178 "$ref": "#/components/schemas/HeroPublic" 

179 } 

180 } 

181 }, 

182 }, 

183 "422": { 

184 "description": "Validation Error", 

185 "content": { 

186 "application/json": { 

187 "schema": { 

188 "$ref": "#/components/schemas/HTTPValidationError" 

189 } 

190 } 

191 }, 

192 }, 

193 }, 

194 }, 

195 "get": { 

196 "summary": "Read Heroes", 

197 "operationId": "read_heroes_heroes__get", 

198 "parameters": [ 

199 { 

200 "name": "offset", 

201 "in": "query", 

202 "required": False, 

203 "schema": { 

204 "type": "integer", 

205 "default": 0, 

206 "title": "Offset", 

207 }, 

208 }, 

209 { 

210 "name": "limit", 

211 "in": "query", 

212 "required": False, 

213 "schema": { 

214 "type": "integer", 

215 "maximum": 100, 

216 "default": 100, 

217 "title": "Limit", 

218 }, 

219 }, 

220 ], 

221 "responses": { 

222 "200": { 

223 "description": "Successful Response", 

224 "content": { 

225 "application/json": { 

226 "schema": { 

227 "type": "array", 

228 "items": { 

229 "$ref": "#/components/schemas/HeroPublic" 

230 }, 

231 "title": "Response Read Heroes Heroes Get", 

232 } 

233 } 

234 }, 

235 }, 

236 "422": { 

237 "description": "Validation Error", 

238 "content": { 

239 "application/json": { 

240 "schema": { 

241 "$ref": "#/components/schemas/HTTPValidationError" 

242 } 

243 } 

244 }, 

245 }, 

246 }, 

247 }, 

248 }, 

249 "/heroes/{hero_id}": { 

250 "get": { 

251 "summary": "Read Hero", 

252 "operationId": "read_hero_heroes__hero_id__get", 

253 "parameters": [ 

254 { 

255 "name": "hero_id", 

256 "in": "path", 

257 "required": True, 

258 "schema": {"type": "integer", "title": "Hero Id"}, 

259 } 

260 ], 

261 "responses": { 

262 "200": { 

263 "description": "Successful Response", 

264 "content": { 

265 "application/json": { 

266 "schema": { 

267 "$ref": "#/components/schemas/HeroPublic" 

268 } 

269 } 

270 }, 

271 }, 

272 "422": { 

273 "description": "Validation Error", 

274 "content": { 

275 "application/json": { 

276 "schema": { 

277 "$ref": "#/components/schemas/HTTPValidationError" 

278 } 

279 } 

280 }, 

281 }, 

282 }, 

283 }, 

284 "patch": { 

285 "summary": "Update Hero", 

286 "operationId": "update_hero_heroes__hero_id__patch", 

287 "parameters": [ 

288 { 

289 "name": "hero_id", 

290 "in": "path", 

291 "required": True, 

292 "schema": {"type": "integer", "title": "Hero Id"}, 

293 } 

294 ], 

295 "requestBody": { 

296 "required": True, 

297 "content": { 

298 "application/json": { 

299 "schema": { 

300 "$ref": "#/components/schemas/HeroUpdate" 

301 } 

302 } 

303 }, 

304 }, 

305 "responses": { 

306 "200": { 

307 "description": "Successful Response", 

308 "content": { 

309 "application/json": { 

310 "schema": { 

311 "$ref": "#/components/schemas/HeroPublic" 

312 } 

313 } 

314 }, 

315 }, 

316 "422": { 

317 "description": "Validation Error", 

318 "content": { 

319 "application/json": { 

320 "schema": { 

321 "$ref": "#/components/schemas/HTTPValidationError" 

322 } 

323 } 

324 }, 

325 }, 

326 }, 

327 }, 

328 "delete": { 

329 "summary": "Delete Hero", 

330 "operationId": "delete_hero_heroes__hero_id__delete", 

331 "parameters": [ 

332 { 

333 "name": "hero_id", 

334 "in": "path", 

335 "required": True, 

336 "schema": {"type": "integer", "title": "Hero Id"}, 

337 } 

338 ], 

339 "responses": { 

340 "200": { 

341 "description": "Successful Response", 

342 "content": {"application/json": {"schema": {}}}, 

343 }, 

344 "422": { 

345 "description": "Validation Error", 

346 "content": { 

347 "application/json": { 

348 "schema": { 

349 "$ref": "#/components/schemas/HTTPValidationError" 

350 } 

351 } 

352 }, 

353 }, 

354 }, 

355 }, 

356 }, 

357 }, 

358 "components": { 

359 "schemas": { 

360 "HTTPValidationError": { 

361 "properties": { 

362 "detail": { 

363 "items": { 

364 "$ref": "#/components/schemas/ValidationError" 

365 }, 

366 "type": "array", 

367 "title": "Detail", 

368 } 

369 }, 

370 "type": "object", 

371 "title": "HTTPValidationError", 

372 }, 

373 "HeroCreate": { 

374 "properties": { 

375 "name": {"type": "string", "title": "Name"}, 

376 "age": IsDict( 

377 { 

378 "anyOf": [{"type": "integer"}, {"type": "null"}], 

379 "title": "Age", 

380 } 

381 ) 

382 | IsDict( 

383 # TODO: remove when deprecating Pydantic v1 

384 { 

385 "type": "integer", 

386 "title": "Age", 

387 } 

388 ), 

389 "secret_name": {"type": "string", "title": "Secret Name"}, 

390 }, 

391 "type": "object", 

392 "required": ["name", "secret_name"], 

393 "title": "HeroCreate", 

394 }, 

395 "HeroPublic": { 

396 "properties": { 

397 "name": {"type": "string", "title": "Name"}, 

398 "age": IsDict( 

399 { 

400 "anyOf": [{"type": "integer"}, {"type": "null"}], 

401 "title": "Age", 

402 } 

403 ) 

404 | IsDict( 

405 # TODO: remove when deprecating Pydantic v1 

406 { 

407 "type": "integer", 

408 "title": "Age", 

409 } 

410 ), 

411 "id": {"type": "integer", "title": "Id"}, 

412 }, 

413 "type": "object", 

414 "required": ["name", "id"], 

415 "title": "HeroPublic", 

416 }, 

417 "HeroUpdate": { 

418 "properties": { 

419 "name": IsDict( 

420 { 

421 "anyOf": [{"type": "string"}, {"type": "null"}], 

422 "title": "Name", 

423 } 

424 ) 

425 | IsDict( 

426 # TODO: remove when deprecating Pydantic v1 

427 { 

428 "type": "string", 

429 "title": "Name", 

430 } 

431 ), 

432 "age": IsDict( 

433 { 

434 "anyOf": [{"type": "integer"}, {"type": "null"}], 

435 "title": "Age", 

436 } 

437 ) 

438 | IsDict( 

439 # TODO: remove when deprecating Pydantic v1 

440 { 

441 "type": "integer", 

442 "title": "Age", 

443 } 

444 ), 

445 "secret_name": IsDict( 

446 { 

447 "anyOf": [{"type": "string"}, {"type": "null"}], 

448 "title": "Secret Name", 

449 } 

450 ) 

451 | IsDict( 

452 # TODO: remove when deprecating Pydantic v1 

453 { 

454 "type": "string", 

455 "title": "Secret Name", 

456 } 

457 ), 

458 }, 

459 "type": "object", 

460 "title": "HeroUpdate", 

461 }, 

462 "ValidationError": { 

463 "properties": { 

464 "loc": { 

465 "items": { 

466 "anyOf": [{"type": "string"}, {"type": "integer"}] 

467 }, 

468 "type": "array", 

469 "title": "Location", 

470 }, 

471 "msg": {"type": "string", "title": "Message"}, 

472 "type": {"type": "string", "title": "Error Type"}, 

473 }, 

474 "type": "object", 

475 "required": ["loc", "msg", "type"], 

476 "title": "ValidationError", 

477 }, 

478 } 

479 }, 

480 } 

481 )