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

61 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 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("tutorial001_py310", marks=needs_py310), 

26 pytest.param("tutorial001_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": 999, 

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, "secret_name": "Dive Wilson", "id": 999, "name": "Dead Pond"} 

71 ) 

72 

73 # Read a hero 

74 hero_id = response.json()["id"] 1abc

75 response = client.get(f"/heroes/{hero_id}") 1abc

76 assert response.status_code == 200, response.text 1abc

77 assert response.json() == snapshot( 1abc

78 {"name": "Dead Pond", "age": 30, "id": 999, "secret_name": "Dive Wilson"} 

79 ) 

80 

81 # Read all heroes 

82 # Create more heroes first 

83 response = client.post( 1abc

84 "/heroes/", 

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

86 ) 

87 assert response.status_code == 200, response.text 1abc

88 response = client.post( 1abc

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

90 ) 

91 assert response.status_code == 200, response.text 1abc

92 

93 response = client.get("/heroes/") 1abc

94 assert response.status_code == 200, response.text 1abc

95 assert response.json() == snapshot( 1abc

96 [ 

97 { 

98 "name": "Dead Pond", 

99 "age": 30, 

100 "id": IsInt(), 

101 "secret_name": "Dive Wilson", 

102 }, 

103 { 

104 "name": "Spider-Boy", 

105 "age": 18, 

106 "id": IsInt(), 

107 "secret_name": "Pedro Parqueador", 

108 }, 

109 { 

110 "name": "Rusty-Man", 

111 "age": None, 

112 "id": IsInt(), 

113 "secret_name": "Tommy Sharp", 

114 }, 

115 ] 

116 ) 

117 

118 response = client.get("/heroes/?offset=1&limit=1") 1abc

119 assert response.status_code == 200, response.text 1abc

120 assert response.json() == snapshot( 1abc

121 [ 

122 { 

123 "name": "Spider-Boy", 

124 "age": 18, 

125 "id": IsInt(), 

126 "secret_name": "Pedro Parqueador", 

127 } 

128 ] 

129 ) 

130 

131 # Delete a hero 

132 response = client.delete(f"/heroes/{hero_id}") 1abc

133 assert response.status_code == 200, response.text 1abc

134 assert response.json() == snapshot({"ok": True}) 1abc

135 

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

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

138 

139 response = client.delete(f"/heroes/{hero_id}") 1abc

140 assert response.status_code == 404, response.text 1abc

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

142 

143 

144def test_openapi_schema(client: TestClient): 1degf

145 response = client.get("/openapi.json") 1hij

146 assert response.status_code == 200, response.text 1hij

147 assert response.json() == snapshot( 1hij

148 { 

149 "openapi": "3.1.0", 

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

151 "paths": { 

152 "/heroes/": { 

153 "post": { 

154 "summary": "Create Hero", 

155 "operationId": "create_hero_heroes__post", 

156 "requestBody": { 

157 "required": True, 

158 "content": { 

159 "application/json": { 

160 "schema": {"$ref": "#/components/schemas/Hero"} 

161 } 

162 }, 

163 }, 

164 "responses": { 

165 "200": { 

166 "description": "Successful Response", 

167 "content": { 

168 "application/json": { 

169 "schema": {"$ref": "#/components/schemas/Hero"} 

170 } 

171 }, 

172 }, 

173 "422": { 

174 "description": "Validation Error", 

175 "content": { 

176 "application/json": { 

177 "schema": { 

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

179 } 

180 } 

181 }, 

182 }, 

183 }, 

184 }, 

185 "get": { 

186 "summary": "Read Heroes", 

187 "operationId": "read_heroes_heroes__get", 

188 "parameters": [ 

189 { 

190 "name": "offset", 

191 "in": "query", 

192 "required": False, 

193 "schema": { 

194 "type": "integer", 

195 "default": 0, 

196 "title": "Offset", 

197 }, 

198 }, 

199 { 

200 "name": "limit", 

201 "in": "query", 

202 "required": False, 

203 "schema": { 

204 "type": "integer", 

205 "maximum": 100, 

206 "default": 100, 

207 "title": "Limit", 

208 }, 

209 }, 

210 ], 

211 "responses": { 

212 "200": { 

213 "description": "Successful Response", 

214 "content": { 

215 "application/json": { 

216 "schema": { 

217 "type": "array", 

218 "items": { 

219 "$ref": "#/components/schemas/Hero" 

220 }, 

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

222 } 

223 } 

224 }, 

225 }, 

226 "422": { 

227 "description": "Validation Error", 

228 "content": { 

229 "application/json": { 

230 "schema": { 

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

232 } 

233 } 

234 }, 

235 }, 

236 }, 

237 }, 

238 }, 

239 "/heroes/{hero_id}": { 

240 "get": { 

241 "summary": "Read Hero", 

242 "operationId": "read_hero_heroes__hero_id__get", 

243 "parameters": [ 

244 { 

245 "name": "hero_id", 

246 "in": "path", 

247 "required": True, 

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

249 } 

250 ], 

251 "responses": { 

252 "200": { 

253 "description": "Successful Response", 

254 "content": { 

255 "application/json": { 

256 "schema": {"$ref": "#/components/schemas/Hero"} 

257 } 

258 }, 

259 }, 

260 "422": { 

261 "description": "Validation Error", 

262 "content": { 

263 "application/json": { 

264 "schema": { 

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

266 } 

267 } 

268 }, 

269 }, 

270 }, 

271 }, 

272 "delete": { 

273 "summary": "Delete Hero", 

274 "operationId": "delete_hero_heroes__hero_id__delete", 

275 "parameters": [ 

276 { 

277 "name": "hero_id", 

278 "in": "path", 

279 "required": True, 

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

281 } 

282 ], 

283 "responses": { 

284 "200": { 

285 "description": "Successful Response", 

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

287 }, 

288 "422": { 

289 "description": "Validation Error", 

290 "content": { 

291 "application/json": { 

292 "schema": { 

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

294 } 

295 } 

296 }, 

297 }, 

298 }, 

299 }, 

300 }, 

301 }, 

302 "components": { 

303 "schemas": { 

304 "HTTPValidationError": { 

305 "properties": { 

306 "detail": { 

307 "items": { 

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

309 }, 

310 "type": "array", 

311 "title": "Detail", 

312 } 

313 }, 

314 "type": "object", 

315 "title": "HTTPValidationError", 

316 }, 

317 "Hero": { 

318 "properties": { 

319 "id": { 

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

321 "title": "Id", 

322 }, 

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

324 "age": { 

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

326 "title": "Age", 

327 }, 

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

329 }, 

330 "type": "object", 

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

332 "title": "Hero", 

333 }, 

334 "ValidationError": { 

335 "properties": { 

336 "ctx": {"title": "Context", "type": "object"}, 

337 "input": {"title": "Input"}, 

338 "loc": { 

339 "items": { 

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

341 }, 

342 "type": "array", 

343 "title": "Location", 

344 }, 

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

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

347 }, 

348 "type": "object", 

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

350 "title": "ValidationError", 

351 }, 

352 } 

353 }, 

354 } 

355 )