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

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 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 "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 ) 

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": 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 ) 

76 

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 ) 

84 

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

96 

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 ) 

121 

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 ) 

134 

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

139 

140 response = client.get(f"/heroes/{hero_id}") 1abcdefg

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

142 

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

146 

147 

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 )